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}