wasmer_vm/instance/allocator.rs
1use super::{Instance, VMInstance};
2use crate::vmcontext::VMTableDefinition;
3use crate::{VMGlobalDefinition, VMMemoryDefinition};
4use std::alloc::{self, Layout};
5use std::convert::TryFrom;
6use std::mem;
7use std::ptr::{self, NonNull};
8use wasmer_types::entity::EntityRef;
9use wasmer_types::{LocalGlobalIndex, VMOffsets};
10use wasmer_types::{LocalMemoryIndex, LocalTableIndex, ModuleInfo};
11
12/// This is an intermediate type that manages the raw allocation and
13/// metadata when creating a [`VMInstance`].
14///
15/// This type will free the allocated memory if it's dropped before
16/// being used.
17///
18/// It is important to remind that [`VMInstance`] is dynamically-sized
19/// based on `VMOffsets`: The `Instance.vmctx` field represents a
20/// dynamically-sized array that extends beyond the nominal end of the
21/// type. So in order to create an instance of it, we must:
22///
23/// 1. Define the correct layout for `Instance` (size and alignment),
24/// 2. Allocate it properly.
25///
26/// The `InstanceAllocator::instance_layout` helper computes the correct
27/// layout to represent the wanted [`VMInstance`].
28///
29/// Then we use this layout to allocate an empty `Instance` properly.
30pub struct InstanceAllocator {
31 /// The buffer that will contain the [`VMInstance`] and dynamic fields.
32 instance_ptr: NonNull<Instance>,
33
34 /// The layout of the `instance_ptr` buffer.
35 instance_layout: Layout,
36
37 /// Information about the offsets into the `instance_ptr` buffer for
38 /// the dynamic fields.
39 offsets: VMOffsets,
40
41 /// Whether or not this type has transferred ownership of the
42 /// `instance_ptr` buffer. If it has not when being dropped,
43 /// the buffer should be freed.
44 consumed: bool,
45}
46
47impl Drop for InstanceAllocator {
48 fn drop(&mut self) {
49 if !self.consumed {
50 // If `consumed` has not been set, then we still have ownership
51 // over the buffer and must free it.
52 let instance_ptr = self.instance_ptr.as_ptr();
53
54 unsafe {
55 std::alloc::dealloc(instance_ptr as *mut u8, self.instance_layout);
56 }
57 }
58 }
59}
60
61impl InstanceAllocator {
62 /// Allocates instance data for use with [`VMInstance::new`].
63 ///
64 /// Returns a wrapper type around the allocation and 2 vectors of
65 /// pointers into the allocated buffer. These lists of pointers
66 /// correspond to the location in memory for the local memories and
67 /// tables respectively. These pointers should be written to before
68 /// calling [`VMInstance::new`].
69 ///
70 /// [`VMInstance::new`]: super::VMInstance::new
71 #[allow(clippy::type_complexity)]
72 pub fn new(
73 module: &ModuleInfo,
74 ) -> Result<
75 (
76 Self,
77 Vec<NonNull<VMMemoryDefinition>>,
78 Vec<NonNull<VMTableDefinition>>,
79 Vec<NonNull<VMGlobalDefinition>>,
80 ),
81 String,
82 > {
83 let offsets = VMOffsets::try_new(mem::size_of::<usize>() as u8, module)?;
84 Ok(Self::new_with_offsets(offsets, module))
85 }
86
87 /// Same as [`InstanceAllocator::new`], but accepts pre-computed
88 /// [`VMOffsets`] instead of computing them from the module.
89 ///
90 /// `VMOffsets::try_new(pointer_size, module)` is deterministic given
91 /// `(pointer_size, module)`, and the `pointer_size` is fixed to
92 /// `size_of::<usize>()` on the host. Callers that instantiate the
93 /// same module repeatedly (per-request wasm hosts: cloud workers,
94 /// CosmWasm, op-vm, …) can compute the offsets once at compile/cache
95 /// time and pass them here to skip the recomputation on every
96 /// `Instance::new`.
97 ///
98 /// # Caller contract
99 ///
100 /// `offsets` MUST equal `VMOffsets::try_new(size_of::<usize>() as u8, module)`
101 /// for the same `module`. Passing offsets computed for a different
102 /// module, or with a different pointer size, will produce an
103 /// incorrectly sized allocation and undefined behavior. Callers that
104 /// cache the offsets should key the cache by the same `ModuleInfo`
105 /// they pass here.
106 #[allow(clippy::type_complexity)]
107 pub fn new_with_offsets(
108 offsets: VMOffsets,
109 module: &ModuleInfo,
110 ) -> (
111 Self,
112 Vec<NonNull<VMMemoryDefinition>>,
113 Vec<NonNull<VMTableDefinition>>,
114 Vec<NonNull<VMGlobalDefinition>>,
115 ) {
116 // Silence unused warning when the body below does not need
117 // `module` directly (it's kept in the signature for API
118 // symmetry with `new` and for future callers).
119 let _ = module;
120 let instance_layout = Self::instance_layout(&offsets);
121
122 #[allow(clippy::cast_ptr_alignment)]
123 let instance_ptr = unsafe { alloc::alloc(instance_layout) as *mut Instance };
124
125 let instance_ptr = if let Some(ptr) = NonNull::new(instance_ptr) {
126 ptr
127 } else {
128 alloc::handle_alloc_error(instance_layout);
129 };
130
131 let allocator = Self {
132 instance_ptr,
133 instance_layout,
134 offsets,
135 consumed: false,
136 };
137
138 // # Safety
139 // Both of these calls are safe because we allocate the pointer
140 // above with the same `offsets` that these functions use.
141 // Thus there will be enough valid memory for both of them.
142 let memories = unsafe { allocator.memory_definition_locations() };
143 let tables = unsafe { allocator.table_definition_locations() };
144 let globals = unsafe { allocator.global_definition_locations() };
145
146 (allocator, memories, tables, globals)
147 }
148
149 /// Calculate the appropriate layout for the internal `Instance` structure.
150 fn instance_layout(offsets: &VMOffsets) -> Layout {
151 let vmctx_size = usize::try_from(offsets.size_of_vmctx())
152 .expect("Failed to convert the size of `vmctx` to a `usize`");
153
154 let instance_vmctx_layout =
155 Layout::array::<u8>(vmctx_size).expect("Failed to create a layout for `VMContext`");
156
157 let (instance_layout, _offset) = Layout::new::<Instance>()
158 .extend(instance_vmctx_layout)
159 .expect("Failed to extend to `Instance` layout to include `VMContext`");
160
161 instance_layout.pad_to_align()
162 }
163
164 /// Get the locations of where the local [`VMMemoryDefinition`]s should be stored.
165 ///
166 /// This function lets us create `Memory` objects on the host with backing
167 /// memory in the VM.
168 ///
169 /// # Safety
170 ///
171 /// - `Self.instance_ptr` must point to enough memory that all of
172 /// the offsets in `Self.offsets` point to valid locations in
173 /// memory, i.e. `Self.instance_ptr` must have been allocated by
174 /// `Self::new`.
175 unsafe fn memory_definition_locations(&self) -> Vec<NonNull<VMMemoryDefinition>> {
176 unsafe {
177 let num_memories = self.offsets.num_local_memories();
178 let num_memories = usize::try_from(num_memories).unwrap();
179 let mut out = Vec::with_capacity(num_memories);
180
181 // We need to do some pointer arithmetic now. The unit is `u8`.
182 let ptr = self.instance_ptr.cast::<u8>().as_ptr();
183 let base_ptr = ptr.add(mem::size_of::<Instance>());
184
185 for i in 0..num_memories {
186 let mem_offset = self
187 .offsets
188 .vmctx_vmmemory_definition(LocalMemoryIndex::new(i));
189 let mem_offset = usize::try_from(mem_offset).unwrap();
190
191 let new_ptr = NonNull::new_unchecked(base_ptr.add(mem_offset));
192
193 out.push(new_ptr.cast());
194 }
195
196 out
197 }
198 }
199
200 /// Get the locations of where the [`VMTableDefinition`]s should be stored.
201 ///
202 /// This function lets us create [`Table`] objects on the host with backing
203 /// memory in the VM.
204 ///
205 /// # Safety
206 ///
207 /// - `Self.instance_ptr` must point to enough memory that all of
208 /// the offsets in `Self.offsets` point to valid locations in
209 /// memory, i.e. `Self.instance_ptr` must have been allocated by
210 /// `Self::new`.
211 unsafe fn table_definition_locations(&self) -> Vec<NonNull<VMTableDefinition>> {
212 unsafe {
213 let num_tables = self.offsets.num_local_tables();
214 let num_tables = usize::try_from(num_tables).unwrap();
215 let mut out = Vec::with_capacity(num_tables);
216
217 // We need to do some pointer arithmetic now. The unit is `u8`.
218 let ptr = self.instance_ptr.cast::<u8>().as_ptr();
219 let base_ptr = ptr.add(std::mem::size_of::<Instance>());
220
221 for i in 0..num_tables {
222 let table_offset = self
223 .offsets
224 .vmctx_vmtable_definition(LocalTableIndex::new(i));
225 let table_offset = usize::try_from(table_offset).unwrap();
226
227 let new_ptr = NonNull::new_unchecked(base_ptr.add(table_offset));
228
229 out.push(new_ptr.cast());
230 }
231 out
232 }
233 }
234
235 /// Get the locations of where the [`VMGlobalDefinition`]s should be stored.
236 ///
237 /// This function lets us create [`Global`] objects on the host with backing
238 /// memory in the VM.
239 ///
240 /// # Safety
241 ///
242 /// - `Self.instance_ptr` must point to enough memory that all of
243 /// the offsets in `Self.offsets` point to valid locations in
244 /// memory, i.e. `Self.instance_ptr` must have been allocated by
245 /// `Self::new`.
246 unsafe fn global_definition_locations(&self) -> Vec<NonNull<VMGlobalDefinition>> {
247 unsafe {
248 let num_globals = self.offsets.num_local_globals();
249 let num_globals = usize::try_from(num_globals).unwrap();
250 let mut out = Vec::with_capacity(num_globals);
251
252 let ptr = self.instance_ptr.cast::<u8>().as_ptr();
253 let base_ptr = ptr.add(std::mem::size_of::<Instance>());
254
255 for i in 0..num_globals {
256 let global_offset = self
257 .offsets
258 .vmctx_vmglobal_definition(LocalGlobalIndex::new(i));
259 let global_offset = usize::try_from(global_offset).unwrap();
260
261 let new_ptr = NonNull::new_unchecked(base_ptr.add(global_offset));
262 out.push(new_ptr.cast());
263 }
264
265 out
266 }
267 }
268
269 /// Finish preparing by writing the internal `Instance` into memory, and
270 /// consume this `InstanceAllocator`.
271 pub(crate) fn into_vminstance(mut self, instance: Instance) -> VMInstance {
272 // Prevent the old state's drop logic from being called as we
273 // transition into the new state.
274 self.consumed = true;
275
276 unsafe {
277 // `instance` is moved at `Self.instance_ptr`. This
278 // pointer has been allocated by `Self::allocate_instance`
279 // (so by `VMInstance::allocate_instance`).
280 ptr::write(self.instance_ptr.as_ptr(), instance);
281 // Now `instance_ptr` is correctly initialized!
282 }
283 let instance = self.instance_ptr;
284 let instance_layout = self.instance_layout;
285
286 // This is correct because of the invariants of `Self` and
287 // because we write `Instance` to the pointer in this function.
288 VMInstance {
289 instance,
290 instance_layout,
291 }
292 }
293
294 /// Get the [`VMOffsets`] for the allocated buffer.
295 pub(crate) fn offsets(&self) -> &VMOffsets {
296 &self.offsets
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use wasmer_types::ModuleInfo;
304
305 /// `VMOffsets::try_new` is deterministic for a given `(pointer_size, module)`.
306 /// The whole VMOffsets-caching optimization assumes this; verify it as
307 /// the test that would catch any future change to `VMOffsets::try_new` that
308 /// introduced non-determinism (e.g. an internal HashMap iteration).
309 #[test]
310 fn vmoffsets_new_is_deterministic_on_empty_module() {
311 let module = ModuleInfo::default();
312 let ps = mem::size_of::<usize>() as u8;
313 let a = VMOffsets::try_new(ps, &module).unwrap();
314 let b = VMOffsets::try_new(ps, &module).unwrap();
315
316 // Use Debug repr for comparison, VMOffsets doesn't impl `PartialEq`
317 // upstream, but its layout is constant for a given input so the
318 // textual debug form is a sufficient identity check.
319 let a_dbg = format!("{a:?}");
320 let b_dbg = format!("{b:?}");
321 assert_eq!(a_dbg, b_dbg, "VMOffsets::try_new must be deterministic");
322 }
323
324 /// `InstanceAllocator::new_with_offsets` and `InstanceAllocator::new`
325 /// must produce allocators with the same `instance_layout` (size +
326 /// alignment) and the same `VMOffsets`. That's the invariant the
327 /// upstream caller (`Artifact::instantiate`) relies on when it
328 /// substitutes the cached value.
329 ///
330 /// The allocator owns a heap allocation; both paths allocate and we
331 /// just drop the resulting allocators at end of test (their `Drop`
332 /// frees the allocation since neither was `consumed`).
333 #[test]
334 fn new_with_offsets_matches_new() {
335 let module = ModuleInfo::default();
336 let ps = mem::size_of::<usize>() as u8;
337
338 let (a, _, _, _) = InstanceAllocator::new(&module).unwrap();
339 let cached = VMOffsets::try_new(ps, &module).unwrap();
340 let (b, _, _, _) = InstanceAllocator::new_with_offsets(cached, &module);
341
342 assert_eq!(
343 a.instance_layout.size(),
344 b.instance_layout.size(),
345 "instance_layout.size() mismatch"
346 );
347 assert_eq!(
348 a.instance_layout.align(),
349 b.instance_layout.align(),
350 "instance_layout.align() mismatch"
351 );
352
353 // Both should report the same `size_of_vmctx` since that's derived
354 // from `VMOffsets` and the same module went in.
355 assert_eq!(
356 a.offsets.size_of_vmctx(),
357 b.offsets.size_of_vmctx(),
358 "size_of_vmctx mismatch, caching broke the layout invariant"
359 );
360 }
361
362 /// Stress: build the allocator many times. Catches any state that
363 /// leaks between calls (e.g. a `Vec` returned in the offsets that
364 /// references a previous module). Each iteration should drop cleanly.
365 #[test]
366 fn new_with_offsets_many_times() {
367 let module = ModuleInfo::default();
368 let ps = mem::size_of::<usize>() as u8;
369 let expected_size = {
370 let (a, _, _, _) = InstanceAllocator::new(&module).unwrap();
371 a.instance_layout.size()
372 };
373
374 for _ in 0..1024 {
375 let cached = VMOffsets::try_new(ps, &module).unwrap();
376 let (b, _, _, _) = InstanceAllocator::new_with_offsets(cached, &module);
377 assert_eq!(b.instance_layout.size(), expected_size);
378 }
379 }
380}