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