Skip to main content

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}