1 //! The deferred reference-counting (DRC) collector.
2 //!
3 //! Warning: this ref-counting collector does not have a tracing cycle
4 //! collector, and therefore cannot collect cycles between GC objects!
5 //!
6 //! For host VM code, we use plain reference counting, where cloning increments
7 //! the reference count, and dropping decrements it. We can avoid many of the
8 //! on-stack increment/decrement operations that typically plague the
9 //! performance of reference counting via Rust's ownership and borrowing system.
10 //! Moving a `VMGcRef` avoids mutating its reference count, and borrowing it
11 //! either avoids the reference count increment or delays it until if/when the
12 //! `VMGcRef` is cloned.
13 //!
14 //! When passing a `VMGcRef` into compiled Wasm code, we don't want to do
15 //! reference count mutations for every compiled `local.{get,set}`, nor for
16 //! every function call. Therefore, we use a variation of **deferred reference
17 //! counting**, where we only mutate reference counts when storing `VMGcRef`s
18 //! somewhere that outlives the Wasm activation: into a global or
19 //! table. Simultaneously, we over-approximate the set of `VMGcRef`s that are
20 //! inside Wasm function activations. Periodically, we walk the stack at GC safe
21 //! points, and use stack map information to precisely identify the set of
22 //! `VMGcRef`s inside Wasm activations. Then we take the difference between this
23 //! precise set and our over-approximation, and decrement the reference count
24 //! for each of the `VMGcRef`s that are in our over-approximation but not in the
25 //! precise set. Finally, the over-approximation is reset to the precise set.
26 //!
27 //! An intrusive, singly-linked list in the object header implements the
28 //! over-approximated set of `VMGcRef`s referenced by Wasm activations. Calling
29 //! a Wasm function and passing it a `VMGcRef` inserts the `VMGcRef` into that
30 //! list if it is not already present, and the compiled Wasm function logically
31 //! "borrows" the `VMGcRef` from the list. Similarly, `global.get` and
32 //! `table.get` operations logically clone the gotten `VMGcRef` into that list
33 //! and then "borrow" the reference out of the list.
34 //!
35 //! When a `VMGcRef` is returned to host code from a Wasm function, the host
36 //! increments the reference count (because the reference is logically
37 //! "borrowed" from the list and the reference count from
38 //! the table will be dropped at the next GC).
39 //!
40 //! The precise set of stack roots is implemented with a mark bit in the object
41 //! header. See the `trace` and `sweep` methods for more details.
42 //!
43 //! For more general information on deferred reference counting, see *An
44 //! Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
45 //! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf>
46 
47 use super::VMArrayRef;
48 use super::free_list::FreeList;
49 use crate::hash_map::HashMap;
50 use crate::hash_set::HashSet;
51 use crate::runtime::vm::{
52     ExternRefHostDataId, ExternRefHostDataTable, GarbageCollection, GcHeap, GcHeapObject,
53     GcProgress, GcRootsIter, GcRuntime, TypedGcRef, VMExternRef, VMGcHeader, VMGcRef,
54 };
55 use crate::vm::VMMemoryDefinition;
56 use crate::{Engine, EngineWeak, prelude::*};
57 use core::sync::atomic::AtomicUsize;
58 use core::{
59     alloc::Layout,
60     any::Any,
61     mem,
62     ops::{Deref, DerefMut},
63     ptr::NonNull,
64 };
65 use wasmtime_environ::drc::{ARRAY_LENGTH_OFFSET, DrcTypeLayouts};
66 use wasmtime_environ::{
67     GcArrayLayout, GcLayout, GcStructLayout, GcTypeLayouts, POISON, VMGcKind, VMSharedTypeIndex,
68     gc_assert,
69 };
70 
71 #[expect(clippy::cast_possible_truncation, reason = "known to not overflow")]
72 const GC_REF_ARRAY_ELEMS_OFFSET: u32 = ARRAY_LENGTH_OFFSET + (mem::size_of::<u32>() as u32);
73 
74 /// The deferred reference-counting (DRC) collector.
75 ///
76 /// This reference-counting collector does not have a cycle collector, and so it
77 /// will not be able to reclaim garbage cycles.
78 ///
79 /// This is not a moving collector; it doesn't have a nursery or do any
80 /// compaction.
81 #[derive(Default)]
82 pub struct DrcCollector {
83     layouts: DrcTypeLayouts,
84 }
85 
86 unsafe impl GcRuntime for DrcCollector {
layouts(&self) -> &dyn GcTypeLayouts87     fn layouts(&self) -> &dyn GcTypeLayouts {
88         &self.layouts
89     }
90 
new_gc_heap(&self, engine: &Engine) -> Result<Box<dyn GcHeap>>91     fn new_gc_heap(&self, engine: &Engine) -> Result<Box<dyn GcHeap>> {
92         let heap = DrcHeap::new(engine)?;
93         Ok(Box::new(heap) as _)
94     }
95 }
96 
97 /// How to trace a GC object.
98 enum TraceInfo {
99     /// How to trace an array.
100     Array {
101         /// Whether this array type's elements are GC references, and need
102         /// tracing.
103         gc_ref_elems: bool,
104     },
105 
106     /// How to trace a struct.
107     Struct {
108         /// The offsets of each GC reference field that needs tracing in
109         /// instances of this struct type.
110         gc_ref_offsets: Box<[u32]>,
111     },
112 }
113 
114 /// A deferred reference-counting (DRC) heap.
115 struct DrcHeap {
116     engine: EngineWeak,
117 
118     /// For every type that we have allocated in this heap, how do we trace it?
119     trace_infos: HashMap<VMSharedTypeIndex, TraceInfo>,
120 
121     /// Count of how many no-gc scopes we are currently within.
122     no_gc_count: u64,
123 
124     /// The head of the over-approximated-stack-roots list.
125     ///
126     /// Note that this is exposed directly to compiled Wasm code through the
127     /// vmctx, so must not move.
128     over_approximated_stack_roots: Box<Option<VMGcRef>>,
129 
130     /// The storage for the GC heap itself.
131     memory: Option<crate::vm::Memory>,
132 
133     /// The cached `VMMemoryDefinition` for `self.memory` so that we don't have
134     /// to make indirect calls through a `dyn RuntimeLinearMemory` object.
135     ///
136     /// Must be updated and kept in sync with `self.memory`, cleared when the
137     /// memory is taken and updated when the memory is replaced.
138     vmmemory: Option<VMMemoryDefinition>,
139 
140     /// A free list describing which ranges of the heap are available for use.
141     free_list: Option<FreeList>,
142 
143     /// An explicit stack to avoid recursion when deallocating one object needs
144     /// to dec-ref another object, which can then be deallocated and dec-refs
145     /// yet another object, etc...
146     ///
147     /// We store this stack here to reuse the storage and avoid repeated
148     /// allocations.
149     ///
150     /// Note that the `Option` is perhaps technically unnecessary (we could
151     /// remove the `Option` and, when we take the stack out of `self`, leave
152     /// behind an empty vec instead of `None`) but we keep it because it will
153     /// help us catch unexpected re-entry, similar to how a `RefCell` would.
154     dec_ref_stack: Option<Vec<VMGcRef>>,
155 }
156 
157 impl DrcHeap {
158     /// Construct a new, default DRC heap.
new(engine: &Engine) -> Result<Self>159     fn new(engine: &Engine) -> Result<Self> {
160         log::trace!("allocating new DRC heap");
161         Ok(Self {
162             engine: engine.weak(),
163             trace_infos: HashMap::with_capacity(1),
164             no_gc_count: 0,
165             over_approximated_stack_roots: Box::new(None),
166             memory: None,
167             vmmemory: None,
168             free_list: None,
169             dec_ref_stack: Some(Vec::with_capacity(1)),
170         })
171     }
172 
engine(&self) -> Engine173     fn engine(&self) -> Engine {
174         self.engine.upgrade().unwrap()
175     }
176 
dealloc(&mut self, gc_ref: VMGcRef)177     fn dealloc(&mut self, gc_ref: VMGcRef) {
178         let drc_ref = drc_ref(&gc_ref);
179         let size = self.index(drc_ref).object_size();
180         let layout = FreeList::layout(size);
181         let index = gc_ref.as_heap_index().unwrap();
182 
183         // Poison the freed memory so that any stale access is detectable.
184         if cfg!(gc_zeal) {
185             let index = usize::try_from(index.get()).unwrap();
186             self.heap_slice_mut()[index..][..layout.size()].fill(POISON);
187         }
188 
189         self.free_list.as_mut().unwrap().dealloc(index, layout);
190     }
191 
192     /// Increment the ref count for the associated object.
inc_ref(&mut self, gc_ref: &VMGcRef)193     fn inc_ref(&mut self, gc_ref: &VMGcRef) {
194         if gc_ref.is_i31() {
195             return;
196         }
197 
198         let drc_ref = drc_ref(gc_ref);
199         let header = self.index_mut(&drc_ref);
200         debug_assert_ne!(
201             header.ref_count, 0,
202             "{:#p} is supposedly live; should have nonzero ref count",
203             *gc_ref
204         );
205         header.ref_count += 1;
206         log::trace!("increment {:#p} ref count -> {}", *gc_ref, header.ref_count);
207     }
208 
209     /// Decrement the ref count for the associated object.
210     ///
211     /// Returns `true` if the ref count reached zero and the object should be
212     /// deallocated.
dec_ref(&mut self, gc_ref: &VMGcRef) -> bool213     fn dec_ref(&mut self, gc_ref: &VMGcRef) -> bool {
214         if gc_ref.is_i31() {
215             return false;
216         }
217 
218         let drc_ref = drc_ref(gc_ref);
219         let header = self.index_mut(drc_ref);
220         debug_assert_ne!(
221             header.ref_count, 0,
222             "{:#p} is supposedly live; should have nonzero ref count",
223             *gc_ref
224         );
225         header.ref_count -= 1;
226         log::trace!("decrement {:#p} ref count -> {}", *gc_ref, header.ref_count);
227         header.ref_count == 0
228     }
229 
230     /// Decrement the ref count for the associated object.
231     ///
232     /// If the ref count reached zero, then deallocate the object and remove its
233     /// associated entry from the `host_data_table` if necessary.
234     ///
235     /// This uses an explicit stack, rather than recursion, for the scenario
236     /// where dropping one object means that the ref count for another object
237     /// that it referenced reaches zero.
dec_ref_and_maybe_dealloc( &mut self, host_data_table: &mut ExternRefHostDataTable, gc_ref: &VMGcRef, )238     fn dec_ref_and_maybe_dealloc(
239         &mut self,
240         host_data_table: &mut ExternRefHostDataTable,
241         gc_ref: &VMGcRef,
242     ) {
243         let mut stack = self.dec_ref_stack.take().unwrap();
244         debug_assert!(stack.is_empty());
245         stack.push(gc_ref.unchecked_copy());
246 
247         while let Some(gc_ref) = stack.pop() {
248             if self.dec_ref(&gc_ref) {
249                 // The object's reference count reached zero.
250                 //
251                 // Enqueue any other objects it references for dec-ref'ing.
252                 self.trace_gc_ref(&gc_ref, &mut stack);
253 
254                 // If this object was an `externref`, remove its associated
255                 // entry from the host-data table.
256                 if let Some(externref) = gc_ref.as_typed::<VMDrcExternRef>(self) {
257                     let host_data_id = self.index(externref).host_data;
258                     host_data_table.dealloc(host_data_id);
259                 }
260 
261                 // Deallocate this GC object!
262                 self.dealloc(gc_ref.unchecked_copy());
263             }
264         }
265 
266         debug_assert!(stack.is_empty());
267         debug_assert!(self.dec_ref_stack.is_none());
268         self.dec_ref_stack = Some(stack);
269     }
270 
271     /// Ensure that we have tracing information for the given type.
ensure_trace_info(&mut self, ty: VMSharedTypeIndex)272     fn ensure_trace_info(&mut self, ty: VMSharedTypeIndex) {
273         if self.trace_infos.contains_key(&ty) {
274             return;
275         }
276 
277         self.insert_new_trace_info(ty);
278     }
279 
insert_new_trace_info(&mut self, ty: VMSharedTypeIndex)280     fn insert_new_trace_info(&mut self, ty: VMSharedTypeIndex) {
281         debug_assert!(!self.trace_infos.contains_key(&ty));
282 
283         let engine = self.engine();
284         let gc_layout = engine
285             .signatures()
286             .layout(ty)
287             .unwrap_or_else(|| panic!("should have a GC layout for {ty:?}"));
288 
289         let info = match gc_layout {
290             GcLayout::Array(l) => {
291                 if l.elems_are_gc_refs {
292                     debug_assert_eq!(l.elem_offset(0), GC_REF_ARRAY_ELEMS_OFFSET,);
293                 }
294                 TraceInfo::Array {
295                     gc_ref_elems: l.elems_are_gc_refs,
296                 }
297             }
298             GcLayout::Struct(l) => TraceInfo::Struct {
299                 gc_ref_offsets: l
300                     .fields
301                     .iter()
302                     .filter_map(|f| if f.is_gc_ref { Some(f.offset) } else { None })
303                     .collect(),
304             },
305         };
306 
307         let old_entry = self.trace_infos.insert(ty, info);
308         debug_assert!(old_entry.is_none());
309     }
310 
311     /// Enumerate all of the given `VMGcRef`'s outgoing edges.
trace_gc_ref(&self, gc_ref: &VMGcRef, stack: &mut Vec<VMGcRef>)312     fn trace_gc_ref(&self, gc_ref: &VMGcRef, stack: &mut Vec<VMGcRef>) {
313         debug_assert!(!gc_ref.is_i31());
314 
315         let header = self.header(gc_ref);
316         let Some(ty) = header.ty() else {
317             debug_assert!(header.kind().matches(VMGcKind::ExternRef));
318             return;
319         };
320 
321         match self
322             .trace_infos
323             .get(&ty)
324             .expect("should have inserted trace info for every GC type allocated in this heap")
325         {
326             TraceInfo::Struct { gc_ref_offsets } => {
327                 stack.reserve(gc_ref_offsets.len());
328                 let data = self.gc_object_data(gc_ref);
329                 for offset in gc_ref_offsets {
330                     let raw = data.read_u32(*offset);
331                     if let Some(gc_ref) = VMGcRef::from_raw_u32(raw)
332                         && !gc_ref.is_i31()
333                     {
334                         debug_assert!(
335                             {
336                                 let header = self.header(&gc_ref);
337                                 let kind = header.kind().as_u32();
338                                 VMGcKind::try_from_u32(kind).is_some()
339                             },
340                             "trace_gc_ref: struct field at offset {offset} references object \
341                              with invalid `VMGcKind`",
342                         );
343 
344                         stack.push(gc_ref);
345                     }
346                 }
347             }
348 
349             TraceInfo::Array { gc_ref_elems } => {
350                 if !*gc_ref_elems {
351                     return;
352                 }
353 
354                 let data = self.gc_object_data(gc_ref);
355                 let len = self.array_len(gc_ref.as_arrayref_unchecked());
356                 stack.reserve(usize::try_from(len).unwrap());
357                 for i in 0..len {
358                     let elem_offset = GC_REF_ARRAY_ELEMS_OFFSET
359                         + i * u32::try_from(mem::size_of::<u32>()).unwrap();
360                     let raw = data.read_u32(elem_offset);
361                     if let Some(gc_ref) = VMGcRef::from_raw_u32(raw)
362                         && !gc_ref.is_i31()
363                     {
364                         debug_assert!(
365                             {
366                                 let header = self.header(&gc_ref);
367                                 let kind = header.kind().as_u32();
368                                 VMGcKind::try_from_u32(kind).is_some()
369                             },
370                             "trace_gc_ref: array element at index {i} references object \
371                              with invalid `VMGcKind`",
372                         );
373 
374                         stack.push(gc_ref);
375                     }
376                 }
377             }
378         }
379     }
380 
381     /// Iterate over the over-approximated-stack-roots list.
iter_over_approximated_stack_roots(&self) -> impl Iterator<Item = VMGcRef> + '_382     fn iter_over_approximated_stack_roots(&self) -> impl Iterator<Item = VMGcRef> + '_ {
383         let mut link = (*self.over_approximated_stack_roots)
384             .as_ref()
385             .map(|r| r.unchecked_copy());
386 
387         core::iter::from_fn(move || {
388             let r = link.as_ref()?.unchecked_copy();
389             link = self.index(drc_ref(&r)).next_over_approximated_stack_root();
390             Some(r)
391         })
392     }
393 
394     /// Assert the integrity of the over-approximated stack roots list.
assert_over_approximated_stack_roots_integrity(&self)395     fn assert_over_approximated_stack_roots_integrity(&self) {
396         if !cfg!(gc_zeal) {
397             return;
398         }
399 
400         let mut visited = HashSet::new();
401         for gc_ref in self.iter_over_approximated_stack_roots() {
402             let idx = gc_ref.as_heap_index().unwrap().get();
403 
404             // Each entry must have a valid `VMGcKind`.
405             let header = self.header(&gc_ref);
406             let kind = header.kind().as_u32();
407             assert!(
408                 VMGcKind::try_from_u32(kind).is_some(),
409                 "over-approx list: entry at heap index {idx} has invalid VMGcKind {kind:#034b}",
410             );
411 
412             // Each entry must have its in-list bit set.
413             let drc_header = self.index(drc_ref(&gc_ref));
414             assert!(
415                 drc_header.is_in_over_approximated_stack_roots(),
416                 "over-approx list: entry at heap index {idx} does not have in-list bit set",
417             );
418 
419             // Each entry must have a nonzero ref count.
420             assert_ne!(
421                 drc_header.ref_count, 0,
422                 "over-approx list: entry at heap index {idx} has zero ref count",
423             );
424 
425             // No cycles or duplicates.
426             assert!(
427                 visited.insert(idx),
428                 "over-approx list: cycle or duplicate detected at heap index {idx}",
429             );
430         }
431     }
432 
433     /// Assert that every free block in the free list is filled with the poison
434     /// pattern.
assert_free_blocks_are_poisoned(&self)435     fn assert_free_blocks_are_poisoned(&self) {
436         if !cfg!(gc_zeal) {
437             return;
438         }
439 
440         let free_list = self.free_list.as_ref().unwrap();
441         for (index, len) in free_list.iter_free_blocks() {
442             let start = usize::try_from(index).unwrap();
443             let size = usize::try_from(len).unwrap();
444             let slice = &self.heap_slice()[start..][..size];
445             assert!(
446                 slice.iter().all(|&b| b == POISON),
447                 "free block at heap index {start} (size {size}) is not fully poisoned",
448             );
449         }
450     }
451 
trace(&mut self, roots: &mut GcRootsIter<'_>)452     fn trace(&mut self, roots: &mut GcRootsIter<'_>) {
453         // The `over_approx_set` is used for `debug_assert!`s checking that
454         // every reference we read out from the stack via stack maps is actually
455         // in the table. If that weren't true, than either we forgot to insert a
456         // reference in the table when passing it into Wasm (a bug) or we are
457         // reading invalid references from the stack (another bug).
458         let mut over_approx_set: DebugOnly<HashSet<_>> = Default::default();
459         if cfg!(debug_assertions) {
460             over_approx_set.extend(self.iter_over_approximated_stack_roots());
461         }
462 
463         for root in roots {
464             if !root.is_on_wasm_stack() {
465                 // We only trace on-Wasm-stack GC roots. These are the
466                 // GC references that we do deferred ref counting for
467                 // and that get inserted into our activations
468                 // table. Other GC roots are managed purely with naive
469                 // ref counting.
470                 continue;
471             }
472 
473             let gc_ref = root.get();
474 
475             if gc_ref.is_i31() {
476                 continue;
477             }
478 
479             log::trace!("Found GC reference on the stack: {gc_ref:#p}");
480 
481             debug_assert!(
482                 over_approx_set.contains(&gc_ref),
483                 "every on-stack gc ref inside a Wasm frame should \
484                  have be in our over-approximated stack roots set, \
485                  but {gc_ref:#p} is not in the set",
486             );
487             debug_assert!(
488                 self.index(drc_ref(&gc_ref))
489                     .is_in_over_approximated_stack_roots(),
490                 "every on-stack gc ref inside a Wasm frame should have \
491                  its in-the-over-approximated-stack-roots-list bit set",
492             );
493             debug_assert_ne!(
494                 self.index_mut(drc_ref(&gc_ref)).ref_count,
495                 0,
496                 "{gc_ref:#p} is on the Wasm stack and therefore should be held \
497                  alive by the over-approximated-stack-roots set; should have \
498                  nonzero ref count",
499             );
500 
501             self.index_mut(drc_ref(&gc_ref)).set_marked();
502         }
503     }
504 
505     #[inline(never)]
506     #[cold]
log_gc_ref_set(prefix: &str, items: impl Iterator<Item = VMGcRef>)507     fn log_gc_ref_set(prefix: &str, items: impl Iterator<Item = VMGcRef>) {
508         assert!(log::log_enabled!(log::Level::Trace));
509         let mut set = "{".to_string();
510         let mut any = false;
511         for gc_ref in items {
512             any = true;
513             set += &format!("\n  {gc_ref:#p},");
514         }
515         if any {
516             set.push('\n');
517         }
518         set.push('}');
519         log::trace!("{prefix}: {set}");
520     }
521 
522     /// Sweep the bump allocation table after we've discovered our precise stack
523     /// roots.
sweep(&mut self, host_data_table: &mut ExternRefHostDataTable)524     fn sweep(&mut self, host_data_table: &mut ExternRefHostDataTable) {
525         if log::log_enabled!(log::Level::Trace) {
526             Self::log_gc_ref_set(
527                 "over-approximated-stack-roots set before sweeping",
528                 self.iter_over_approximated_stack_roots(),
529             );
530         }
531 
532         // Logically, we are taking the difference between
533         // over-approximated-stack-roots set and the precise-stack-roots set,
534         // decrementing the ref count for each object in that difference
535         // (because they are no longer live on the stack), and then resetting
536         // the over-approximated-stack-roots set to the precise set. In our
537         // actual implementation, the over-approximated-stack-roots set is
538         // implemented as an intrusive, singly-linked list in the object
539         // headers, and the precise-stack-roots set is implemented via the mark
540         // bits in the object headers. Therefore, we walk the
541         // over-approximated-stack-roots list, checking whether each object has
542         // its mark bit set.
543         //
544         // * If the mark bit is set, then it is in the precise-stack-roots set
545         //   and is still on the stack, so we keep it in the
546         //   over-approximated-stack-roots list and do not modify its ref count.
547         //
548         // * If the mark bit is not set, then it is not in the
549         //   precise-stack-roots set and is no longer on the stack, so we remove
550         //   it from the over-approximated-stack-roots set and decrement its ref
551         //   count.
552         //
553         // We also clear the mark bits as we do this traversal.
554         //
555         // Finally, note that decrementing ref counts may run `Drop`
556         // implementations, which may run arbitrary user code. However, because
557         // of our `&mut` borrow on this heap (which ultimately comes from a
558         // `&mut Store`) we're guaranteed that nothing will reentrantly touch
559         // this heap or run Wasm code in this store.
560         log::trace!("Begin sweeping");
561 
562         // The `VMGcRef` of the previous object in the
563         // over-approximated-stack-roots list, if any.
564         let mut prev = None;
565 
566         // The `VMGcRef` of the next object in the over-approximated-stack-roots
567         // list, if any.
568         let mut next = (*self.over_approximated_stack_roots)
569             .as_ref()
570             .map(|r| r.unchecked_copy());
571 
572         while let Some(gc_ref) = next {
573             log::trace!("sweeping gc ref: {gc_ref:#p}");
574 
575             let header = self.index_mut(drc_ref(&gc_ref));
576             debug_assert!(header.is_in_over_approximated_stack_roots());
577 
578             if header.clear_marked() {
579                 // This GC ref was marked, meaning it is still on the stack, so
580                 // keep it in the over-approximated-stack-roots list and move on
581                 // to the next object in the list.
582                 log::trace!(
583                     "  -> {gc_ref:#p} is marked, leaving it in the over-approximated-\
584                      stack-roots list"
585                 );
586                 next = header.next_over_approximated_stack_root();
587                 prev = Some(gc_ref);
588                 continue;
589             }
590 
591             // This GC ref was not marked, meaning it is no longer on the stack,
592             // so remove it from the over-approximated-stack-roots list and
593             // decrement its reference count.
594             log::trace!(
595                 "  -> {gc_ref:#p} is not marked, removing it from over-approximated-\
596                  stack-roots list and decrementing its ref count"
597             );
598             next = header.next_over_approximated_stack_root();
599             let prev_next = header.next_over_approximated_stack_root();
600             header.set_in_over_approximated_stack_roots_bit(false);
601             match &prev {
602                 None => *self.over_approximated_stack_roots = prev_next,
603                 Some(prev) => self
604                     .index_mut(drc_ref(prev))
605                     .set_next_over_approximated_stack_root(prev_next),
606             }
607             self.dec_ref_and_maybe_dealloc(host_data_table, &gc_ref);
608         }
609 
610         log::trace!("Done sweeping");
611 
612         if log::log_enabled!(log::Level::Trace) {
613             Self::log_gc_ref_set(
614                 "over-approximated-stack-roots set after sweeping",
615                 self.iter_over_approximated_stack_roots(),
616             );
617         }
618     }
619 }
620 
621 /// Convert the given GC reference as a typed GC reference pointing to a
622 /// `VMDrcHeader`.
drc_ref(gc_ref: &VMGcRef) -> &TypedGcRef<VMDrcHeader>623 fn drc_ref(gc_ref: &VMGcRef) -> &TypedGcRef<VMDrcHeader> {
624     debug_assert!(!gc_ref.is_i31());
625     gc_ref.as_typed_unchecked()
626 }
627 
628 /// Convert a generic `externref` to a typed reference to our concrete
629 /// `externref` type.
externref_to_drc(externref: &VMExternRef) -> &TypedGcRef<VMDrcExternRef>630 fn externref_to_drc(externref: &VMExternRef) -> &TypedGcRef<VMDrcExternRef> {
631     let gc_ref = externref.as_gc_ref();
632     debug_assert!(!gc_ref.is_i31());
633     gc_ref.as_typed_unchecked()
634 }
635 
636 /// The common header for all objects in the DRC collector.
637 ///
638 /// This adds a ref count on top collector-agnostic `VMGcHeader`.
639 ///
640 /// This is accessed by JIT code.
641 #[repr(C)]
642 struct VMDrcHeader {
643     header: VMGcHeader,
644     ref_count: u64,
645     next_over_approximated_stack_root: Option<VMGcRef>,
646     object_size: u32,
647 }
648 
649 unsafe impl GcHeapObject for VMDrcHeader {
650     #[inline]
is(_header: &VMGcHeader) -> bool651     fn is(_header: &VMGcHeader) -> bool {
652         // All DRC objects have a DRC header.
653         true
654     }
655 }
656 
657 impl VMDrcHeader {
658     /// The size of this header's object.
659     #[inline]
object_size(&self) -> usize660     fn object_size(&self) -> usize {
661         usize::try_from(self.object_size).unwrap()
662     }
663 
664     /// Is this object in the over-approximated stack roots list?
665     #[inline]
is_in_over_approximated_stack_roots(&self) -> bool666     fn is_in_over_approximated_stack_roots(&self) -> bool {
667         self.header.reserved_u26() & wasmtime_environ::drc::HEADER_IN_OVER_APPROX_LIST_BIT != 0
668     }
669 
670     /// Set whether this object is in the over-approximated stack roots list.
671     #[inline]
set_in_over_approximated_stack_roots_bit(&mut self, bit: bool)672     fn set_in_over_approximated_stack_roots_bit(&mut self, bit: bool) {
673         let reserved = self.header.reserved_u26();
674         let new_reserved = if bit {
675             reserved | wasmtime_environ::drc::HEADER_IN_OVER_APPROX_LIST_BIT
676         } else {
677             reserved & !wasmtime_environ::drc::HEADER_IN_OVER_APPROX_LIST_BIT
678         };
679         self.header.set_reserved_u26(new_reserved);
680     }
681 
682     /// Get the next object after this one in the over-approximated-stack-roots
683     /// list, if any.
684     #[inline]
next_over_approximated_stack_root(&self) -> Option<VMGcRef>685     fn next_over_approximated_stack_root(&self) -> Option<VMGcRef> {
686         debug_assert!(self.is_in_over_approximated_stack_roots());
687         self.next_over_approximated_stack_root
688             .as_ref()
689             .map(|r| r.unchecked_copy())
690     }
691 
692     /// Set the next object after this one in the over-approximated-stack-roots
693     /// list.
694     #[inline]
set_next_over_approximated_stack_root(&mut self, next: Option<VMGcRef>)695     fn set_next_over_approximated_stack_root(&mut self, next: Option<VMGcRef>) {
696         debug_assert!(self.is_in_over_approximated_stack_roots());
697         self.next_over_approximated_stack_root = next;
698     }
699 
700     /// Is this object marked?
701     #[inline]
is_marked(&self) -> bool702     fn is_marked(&self) -> bool {
703         self.header.reserved_u26() & wasmtime_environ::drc::HEADER_MARK_BIT != 0
704     }
705 
706     /// Mark this object.
707     ///
708     /// Returns `true` if this object was newly marked (i.e. `is_marked()` would
709     /// have returned `false` before this call was made).
710     #[inline]
set_marked(&mut self)711     fn set_marked(&mut self) {
712         let reserved = self.header.reserved_u26();
713         self.header
714             .set_reserved_u26(reserved | wasmtime_environ::drc::HEADER_MARK_BIT);
715     }
716 
717     /// Clear the mark bit for this object.
718     ///
719     /// Returns `true` if this object was marked before the mark bit was
720     /// cleared.
721     #[inline]
clear_marked(&mut self) -> bool722     fn clear_marked(&mut self) -> bool {
723         if self.is_marked() {
724             let reserved = self.header.reserved_u26();
725             self.header
726                 .set_reserved_u26(reserved & !wasmtime_environ::drc::HEADER_MARK_BIT);
727             debug_assert!(!self.is_marked());
728             true
729         } else {
730             false
731         }
732     }
733 }
734 
735 /// The common header for all arrays in the DRC collector.
736 #[repr(C)]
737 struct VMDrcArrayHeader {
738     header: VMDrcHeader,
739     length: u32,
740 }
741 
742 unsafe impl GcHeapObject for VMDrcArrayHeader {
743     #[inline]
is(header: &VMGcHeader) -> bool744     fn is(header: &VMGcHeader) -> bool {
745         header.kind() == VMGcKind::ArrayRef
746     }
747 }
748 
749 /// The representation of an `externref` in the DRC collector.
750 #[repr(C)]
751 struct VMDrcExternRef {
752     header: VMDrcHeader,
753     host_data: ExternRefHostDataId,
754 }
755 
756 unsafe impl GcHeapObject for VMDrcExternRef {
757     #[inline]
is(header: &VMGcHeader) -> bool758     fn is(header: &VMGcHeader) -> bool {
759         header.kind() == VMGcKind::ExternRef
760     }
761 }
762 
763 unsafe impl GcHeap for DrcHeap {
is_attached(&self) -> bool764     fn is_attached(&self) -> bool {
765         debug_assert_eq!(self.memory.is_some(), self.free_list.is_some());
766         debug_assert_eq!(self.memory.is_some(), self.vmmemory.is_some());
767         self.memory.is_some()
768     }
769 
attach(&mut self, memory: crate::vm::Memory)770     fn attach(&mut self, memory: crate::vm::Memory) {
771         assert!(!self.is_attached());
772         assert!(!memory.is_shared_memory());
773         debug_assert!(self.over_approximated_stack_roots.is_none());
774         let len = memory.vmmemory().current_length();
775         self.free_list = Some(FreeList::new(len));
776         self.vmmemory = Some(memory.vmmemory());
777         self.memory = Some(memory);
778 
779         // Poison the entire heap so any access to uninitialized memory is
780         // detectable.
781         if cfg!(gc_zeal) {
782             self.heap_slice_mut().fill(POISON);
783         }
784     }
785 
detach(&mut self) -> crate::vm::Memory786     fn detach(&mut self) -> crate::vm::Memory {
787         assert!(self.is_attached());
788 
789         let DrcHeap {
790             engine: _,
791             no_gc_count,
792             over_approximated_stack_roots,
793             free_list,
794             dec_ref_stack,
795             memory,
796             vmmemory,
797 
798             // NB: we will only ever be reused with the same engine, so no need
799             // to clear out our tracing info just to fill it back in with the
800             // same exact stuff.
801             trace_infos: _,
802         } = self;
803 
804         *no_gc_count = 0;
805         **over_approximated_stack_roots = None;
806         *free_list = None;
807         *vmmemory = None;
808         debug_assert!(dec_ref_stack.as_ref().is_some_and(|s| s.is_empty()));
809 
810         memory.take().unwrap()
811     }
812 
as_any(&self) -> &dyn Any813     fn as_any(&self) -> &dyn Any {
814         self as _
815     }
816 
as_any_mut(&mut self) -> &mut dyn Any817     fn as_any_mut(&mut self) -> &mut dyn Any {
818         self as _
819     }
820 
enter_no_gc_scope(&mut self)821     fn enter_no_gc_scope(&mut self) {
822         self.no_gc_count += 1;
823     }
824 
exit_no_gc_scope(&mut self)825     fn exit_no_gc_scope(&mut self) {
826         self.no_gc_count -= 1;
827     }
828 
clone_gc_ref(&mut self, gc_ref: &VMGcRef) -> VMGcRef829     fn clone_gc_ref(&mut self, gc_ref: &VMGcRef) -> VMGcRef {
830         self.inc_ref(gc_ref);
831         gc_ref.unchecked_copy()
832     }
833 
write_gc_ref( &mut self, host_data_table: &mut ExternRefHostDataTable, destination: &mut Option<VMGcRef>, source: Option<&VMGcRef>, )834     fn write_gc_ref(
835         &mut self,
836         host_data_table: &mut ExternRefHostDataTable,
837         destination: &mut Option<VMGcRef>,
838         source: Option<&VMGcRef>,
839     ) {
840         // Increment the ref count of the object being written into the slot.
841         if let Some(src) = source {
842             self.inc_ref(src);
843         }
844 
845         // Decrement the ref count of the value being overwritten and, if
846         // necessary, deallocate the GC object.
847         if let Some(dest) = destination {
848             self.dec_ref_and_maybe_dealloc(host_data_table, dest);
849         }
850 
851         // Do the actual write.
852         *destination = source.map(|s| s.unchecked_copy());
853     }
854 
expose_gc_ref_to_wasm(&mut self, gc_ref: VMGcRef)855     fn expose_gc_ref_to_wasm(&mut self, gc_ref: VMGcRef) {
856         let header = self.index_mut(drc_ref(&gc_ref));
857         if header.is_in_over_approximated_stack_roots() {
858             // Already in the over-approximated-stack-roots list, nothing more
859             // to do here.
860             return;
861         }
862 
863         // Push this object onto the head of the over-approximated-stack-roots
864         // list.
865         header.set_in_over_approximated_stack_roots_bit(true);
866         let next = (*self.over_approximated_stack_roots)
867             .as_ref()
868             .map(|r| r.unchecked_copy());
869         self.index_mut(drc_ref(&gc_ref))
870             .set_next_over_approximated_stack_root(next);
871         *self.over_approximated_stack_roots = Some(gc_ref);
872     }
873 
alloc_externref( &mut self, host_data: ExternRefHostDataId, ) -> Result<Result<VMExternRef, u64>>874     fn alloc_externref(
875         &mut self,
876         host_data: ExternRefHostDataId,
877     ) -> Result<Result<VMExternRef, u64>> {
878         let gc_ref =
879             match self.alloc_raw(VMGcHeader::externref(), Layout::new::<VMDrcExternRef>())? {
880                 Err(n) => return Ok(Err(n)),
881                 Ok(gc_ref) => gc_ref,
882             };
883         self.index_mut::<VMDrcExternRef>(gc_ref.as_typed_unchecked())
884             .host_data = host_data;
885         Ok(Ok(gc_ref.into_externref_unchecked()))
886     }
887 
externref_host_data(&self, externref: &VMExternRef) -> ExternRefHostDataId888     fn externref_host_data(&self, externref: &VMExternRef) -> ExternRefHostDataId {
889         let typed_ref = externref_to_drc(externref);
890         self.index(typed_ref).host_data
891     }
892 
header(&self, gc_ref: &VMGcRef) -> &VMGcHeader893     fn header(&self, gc_ref: &VMGcRef) -> &VMGcHeader {
894         let header: &VMGcHeader = self.index(gc_ref.as_typed_unchecked());
895 
896         debug_assert!(
897             VMGcKind::try_from_u32(header.kind().as_u32()).is_some(),
898             "header: invalid VMGcKind {:#010x} at gc_ref {gc_ref:#p}",
899             header.kind().as_u32(),
900         );
901 
902         header
903     }
904 
header_mut(&mut self, gc_ref: &VMGcRef) -> &mut VMGcHeader905     fn header_mut(&mut self, gc_ref: &VMGcRef) -> &mut VMGcHeader {
906         let header: &mut VMGcHeader = self.index_mut(gc_ref.as_typed_unchecked());
907 
908         debug_assert!(
909             VMGcKind::try_from_u32(header.kind().as_u32()).is_some(),
910             "header_mut: invalid VMGcKind {:#010x} at gc_ref {gc_ref:#p}",
911             header.kind().as_u32(),
912         );
913 
914         header
915     }
916 
object_size(&self, gc_ref: &VMGcRef) -> usize917     fn object_size(&self, gc_ref: &VMGcRef) -> usize {
918         self.index(drc_ref(gc_ref)).object_size()
919     }
920 
alloc_raw(&mut self, header: VMGcHeader, layout: Layout) -> Result<Result<VMGcRef, u64>>921     fn alloc_raw(&mut self, header: VMGcHeader, layout: Layout) -> Result<Result<VMGcRef, u64>> {
922         debug_assert!(layout.size() >= core::mem::size_of::<VMDrcHeader>());
923         debug_assert!(layout.align() >= core::mem::align_of::<VMDrcHeader>());
924         debug_assert_eq!(header.reserved_u26(), 0);
925 
926         // We must have trace info for every GC type that we allocate in this
927         // heap. The only kinds of GC objects we allocate that do not have an
928         // associated `VMSharedTypeIndex` are `externref`s, and they don't have
929         // any GC edges.
930         if let Some(ty) = header.ty() {
931             self.ensure_trace_info(ty);
932         } else {
933             debug_assert_eq!(header.kind(), VMGcKind::ExternRef);
934         }
935 
936         let object_size = u32::try_from(layout.size()).unwrap();
937 
938         let gc_ref = match self.free_list.as_mut().unwrap().alloc(layout)? {
939             None => return Ok(Err(u64::try_from(layout.size()).unwrap())),
940             Some(index) => VMGcRef::from_heap_index(index).unwrap(),
941         };
942 
943         // Assert that the newly-allocated memory is still filled with the
944         // poison pattern, and hasn't been corrupted since deallocation (or
945         // initial heap creation).
946         if cfg!(gc_zeal) {
947             let start = usize::try_from(gc_ref.as_heap_index().unwrap().get()).unwrap();
948             let slice = &self.heap_slice()[start..][..layout.size()];
949             gc_assert!(
950                 slice.iter().all(|&b| b == POISON),
951                 "newly allocated GC object at index {start} is not fully poisoned; \
952                  freed memory was corrupted",
953             );
954         }
955 
956         *self.index_mut(drc_ref(&gc_ref)) = VMDrcHeader {
957             header,
958             ref_count: 1,
959             next_over_approximated_stack_root: None,
960             object_size,
961         };
962         log::trace!("new object: increment {gc_ref:#p} ref count -> 1");
963         Ok(Ok(gc_ref))
964     }
965 
alloc_uninit_struct_or_exn( &mut self, ty: VMSharedTypeIndex, layout: &GcStructLayout, ) -> Result<Result<VMGcRef, u64>>966     fn alloc_uninit_struct_or_exn(
967         &mut self,
968         ty: VMSharedTypeIndex,
969         layout: &GcStructLayout,
970     ) -> Result<Result<VMGcRef, u64>> {
971         let kind = if layout.is_exception {
972             VMGcKind::ExnRef
973         } else {
974             VMGcKind::StructRef
975         };
976         let gc_ref =
977             match self.alloc_raw(VMGcHeader::from_kind_and_index(kind, ty), layout.layout())? {
978                 Err(n) => return Ok(Err(n)),
979                 Ok(gc_ref) => gc_ref,
980             };
981 
982         Ok(Ok(gc_ref))
983     }
984 
dealloc_uninit_struct_or_exn(&mut self, gcref: VMGcRef)985     fn dealloc_uninit_struct_or_exn(&mut self, gcref: VMGcRef) {
986         self.dealloc(gcref);
987     }
988 
alloc_uninit_array( &mut self, ty: VMSharedTypeIndex, length: u32, layout: &GcArrayLayout, ) -> Result<Result<VMArrayRef, u64>>989     fn alloc_uninit_array(
990         &mut self,
991         ty: VMSharedTypeIndex,
992         length: u32,
993         layout: &GcArrayLayout,
994     ) -> Result<Result<VMArrayRef, u64>> {
995         let gc_ref = match self.alloc_raw(
996             VMGcHeader::from_kind_and_index(VMGcKind::ArrayRef, ty),
997             layout.layout(length),
998         )? {
999             Err(n) => return Ok(Err(n)),
1000             Ok(gc_ref) => gc_ref,
1001         };
1002 
1003         self.index_mut(gc_ref.as_typed_unchecked::<VMDrcArrayHeader>())
1004             .length = length;
1005 
1006         Ok(Ok(gc_ref.into_arrayref_unchecked()))
1007     }
1008 
dealloc_uninit_array(&mut self, arrayref: VMArrayRef)1009     fn dealloc_uninit_array(&mut self, arrayref: VMArrayRef) {
1010         self.dealloc(arrayref.into())
1011     }
1012 
array_len(&self, arrayref: &VMArrayRef) -> u321013     fn array_len(&self, arrayref: &VMArrayRef) -> u32 {
1014         debug_assert!(arrayref.as_gc_ref().is_typed::<VMDrcArrayHeader>(self));
1015         self.index::<VMDrcArrayHeader>(arrayref.as_gc_ref().as_typed_unchecked())
1016             .length
1017     }
1018 
gc<'a>( &'a mut self, roots: GcRootsIter<'a>, host_data_table: &'a mut ExternRefHostDataTable, ) -> Box<dyn GarbageCollection<'a> + 'a>1019     fn gc<'a>(
1020         &'a mut self,
1021         roots: GcRootsIter<'a>,
1022         host_data_table: &'a mut ExternRefHostDataTable,
1023     ) -> Box<dyn GarbageCollection<'a> + 'a> {
1024         assert_eq!(self.no_gc_count, 0, "Cannot GC inside a no-GC scope!");
1025         Box::new(DrcCollection {
1026             roots,
1027             host_data_table,
1028             heap: self,
1029             phase: DrcCollectionPhase::Trace,
1030         })
1031     }
1032 
vmctx_gc_heap_data(&self) -> NonNull<u8>1033     unsafe fn vmctx_gc_heap_data(&self) -> NonNull<u8> {
1034         let ptr: NonNull<Option<VMGcRef>> = NonNull::from(&*self.over_approximated_stack_roots);
1035         ptr.cast()
1036     }
1037 
take_memory(&mut self) -> crate::vm::Memory1038     fn take_memory(&mut self) -> crate::vm::Memory {
1039         debug_assert!(self.is_attached());
1040         self.vmmemory.take();
1041         self.memory.take().unwrap()
1042     }
1043 
replace_memory(&mut self, memory: crate::vm::Memory, delta_bytes_grown: u64)1044     unsafe fn replace_memory(&mut self, memory: crate::vm::Memory, delta_bytes_grown: u64) {
1045         debug_assert!(self.memory.is_none());
1046         debug_assert!(!memory.is_shared_memory());
1047         self.vmmemory = Some(memory.vmmemory());
1048         self.memory = Some(memory);
1049 
1050         // Poison the newly-grown region so stale accesses are detectable.
1051         if cfg!(gc_zeal) {
1052             let old_cap = self.free_list.as_ref().unwrap().current_capacity();
1053             let new_bytes = usize::try_from(delta_bytes_grown).unwrap();
1054             let slice = self.heap_slice_mut();
1055             if old_cap + new_bytes <= slice.len() {
1056                 slice[old_cap..old_cap + new_bytes].fill(POISON);
1057             }
1058         }
1059 
1060         self.free_list
1061             .as_mut()
1062             .unwrap()
1063             .add_capacity(usize::try_from(delta_bytes_grown).unwrap())
1064     }
1065 
1066     #[inline]
vmmemory(&self) -> VMMemoryDefinition1067     fn vmmemory(&self) -> VMMemoryDefinition {
1068         debug_assert!(self.is_attached());
1069         debug_assert!(!self.memory.as_ref().unwrap().is_shared_memory());
1070         let vmmemory = self.vmmemory.as_ref().unwrap();
1071         VMMemoryDefinition {
1072             base: vmmemory.base,
1073             current_length: AtomicUsize::new(vmmemory.current_length()),
1074         }
1075     }
1076 }
1077 
1078 struct DrcCollection<'a> {
1079     roots: GcRootsIter<'a>,
1080     host_data_table: &'a mut ExternRefHostDataTable,
1081     heap: &'a mut DrcHeap,
1082     phase: DrcCollectionPhase,
1083 }
1084 
1085 enum DrcCollectionPhase {
1086     Trace,
1087     Sweep,
1088     Done,
1089 }
1090 
1091 impl<'a> GarbageCollection<'a> for DrcCollection<'a> {
collect_increment(&mut self) -> GcProgress1092     fn collect_increment(&mut self) -> GcProgress {
1093         match self.phase {
1094             DrcCollectionPhase::Trace => {
1095                 log::trace!("Begin DRC trace");
1096 
1097                 self.heap.assert_over_approximated_stack_roots_integrity();
1098                 self.heap.assert_free_blocks_are_poisoned();
1099 
1100                 self.heap.trace(&mut self.roots);
1101 
1102                 self.heap.assert_over_approximated_stack_roots_integrity();
1103                 self.heap.assert_free_blocks_are_poisoned();
1104 
1105                 log::trace!("End DRC trace");
1106                 self.phase = DrcCollectionPhase::Sweep;
1107                 GcProgress::Continue
1108             }
1109             DrcCollectionPhase::Sweep => {
1110                 log::trace!("Begin DRC sweep");
1111 
1112                 self.heap.assert_over_approximated_stack_roots_integrity();
1113                 self.heap.assert_free_blocks_are_poisoned();
1114 
1115                 self.heap.sweep(self.host_data_table);
1116 
1117                 self.heap.assert_over_approximated_stack_roots_integrity();
1118                 self.heap.assert_free_blocks_are_poisoned();
1119 
1120                 log::trace!("End DRC sweep");
1121                 self.phase = DrcCollectionPhase::Done;
1122                 GcProgress::Complete
1123             }
1124             DrcCollectionPhase::Done => GcProgress::Complete,
1125         }
1126     }
1127 }
1128 
1129 #[derive(Debug, Default)]
1130 struct DebugOnly<T> {
1131     inner: T,
1132 }
1133 
1134 impl<T> Deref for DebugOnly<T> {
1135     type Target = T;
1136 
deref(&self) -> &T1137     fn deref(&self) -> &T {
1138         if cfg!(debug_assertions) {
1139             &self.inner
1140         } else {
1141             panic!(
1142                 "only deref `DebugOnly` when `cfg(debug_assertions)` or \
1143                  inside a `debug_assert!(..)`"
1144             )
1145         }
1146     }
1147 }
1148 
1149 impl<T> DerefMut for DebugOnly<T> {
deref_mut(&mut self) -> &mut T1150     fn deref_mut(&mut self) -> &mut T {
1151         if cfg!(debug_assertions) {
1152             &mut self.inner
1153         } else {
1154             panic!(
1155                 "only deref `DebugOnly` when `cfg(debug_assertions)` or \
1156                  inside a `debug_assert!(..)`"
1157             )
1158         }
1159     }
1160 }
1161 
1162 #[cfg(test)]
1163 mod tests {
1164     use super::*;
1165     use wasmtime_environ::HostPtr;
1166 
1167     #[test]
vm_drc_header_size_align()1168     fn vm_drc_header_size_align() {
1169         assert_eq!(
1170             (wasmtime_environ::drc::HEADER_SIZE as usize),
1171             core::mem::size_of::<VMDrcHeader>()
1172         );
1173         assert_eq!(
1174             (wasmtime_environ::drc::HEADER_ALIGN as usize),
1175             core::mem::align_of::<VMDrcHeader>()
1176         );
1177     }
1178 
1179     #[test]
vm_drc_array_header_length_offset()1180     fn vm_drc_array_header_length_offset() {
1181         assert_eq!(
1182             wasmtime_environ::drc::ARRAY_LENGTH_OFFSET,
1183             u32::try_from(core::mem::offset_of!(VMDrcArrayHeader, length)).unwrap(),
1184         );
1185     }
1186 
1187     #[test]
ref_count_is_at_correct_offset()1188     fn ref_count_is_at_correct_offset() {
1189         let extern_data = VMDrcHeader {
1190             header: VMGcHeader::externref(),
1191             ref_count: 0,
1192             next_over_approximated_stack_root: None,
1193             object_size: 0,
1194         };
1195 
1196         let extern_data_ptr = &extern_data as *const _;
1197         let ref_count_ptr = &extern_data.ref_count as *const _;
1198 
1199         let actual_offset = (ref_count_ptr as usize) - (extern_data_ptr as usize);
1200 
1201         let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
1202             ptr: HostPtr,
1203             num_imported_functions: 0,
1204             num_imported_tables: 0,
1205             num_imported_memories: 0,
1206             num_imported_globals: 0,
1207             num_imported_tags: 0,
1208             num_defined_tables: 0,
1209             num_defined_memories: 0,
1210             num_owned_memories: 0,
1211             num_defined_globals: 0,
1212             num_defined_tags: 0,
1213             num_escaped_funcs: 0,
1214         });
1215 
1216         assert_eq!(
1217             offsets.vm_drc_header_ref_count(),
1218             u32::try_from(actual_offset).unwrap(),
1219         );
1220     }
1221 }
1222