Skip to content

Commit dad8432

Browse files
authored
Add GC zeal assertions (#12933)
* Add GC zeal assertions - Poison freed GC objects and new heap memory - Assert newly-allocated objects are filled with poison pattern - Add `gc_assert!` checks for valid `VMGcKind` on `GcHeap::index[_mut]` - Add `gc_assert!` checks for valid `VMGcKind` during tracing - Add `VMGcKind::try_from_u32()` for fallible kind validation - Add over-approximated stack roots list integrity checks, called before and after trace and sweep. Validates kind, in-list bit, ref count, and that the list is not cyclic. - Add assertion that all free blocks of memory contain the poison pattern, before and after trace and sweep * review feedback
1 parent f960e9a commit dad8432

6 files changed

Lines changed: 295 additions & 21 deletions

File tree

crates/cranelift/src/func_environ/gc/enabled.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,55 @@ fn unbarriered_store_gc_ref(
9494
Ok(())
9595
}
9696

97-
/// Emit code to read a struct field or array element from its raw address in
98-
/// the GC heap.
97+
/// Emit inline CLIF code that asserts an object's `VMGcKind` matches the
98+
/// expected kind. Only emits code when `cfg(gc_zeal)` is enabled.
99+
///
100+
/// `gc_ref` must be a non-null, non-i31 GC reference (i32 heap index).
101+
fn emit_gc_kind_assert(
102+
func_env: &mut FuncEnvironment<'_>,
103+
builder: &mut FunctionBuilder<'_>,
104+
gc_ref: ir::Value,
105+
expected_kind: VMGcKind,
106+
) {
107+
if !cfg!(gc_zeal) {
108+
return;
109+
}
110+
111+
func_env.trapz(builder, gc_ref, crate::TRAP_NULL_REFERENCE);
112+
113+
let kind_addr = func_env.prepare_gc_ref_access(
114+
builder,
115+
gc_ref,
116+
BoundsCheck::StaticObjectField {
117+
offset: wasmtime_environ::VM_GC_HEADER_KIND_OFFSET,
118+
access_size: wasmtime_environ::VM_GC_KIND_SIZE,
119+
object_size: wasmtime_environ::VM_GC_HEADER_SIZE,
120+
},
121+
);
122+
let kind_and_reserved_bits = builder.ins().load(
123+
ir::types::I32,
124+
ir::MemFlags::trusted().with_readonly(),
125+
kind_addr,
126+
0,
127+
);
128+
let kind_mask = builder
129+
.ins()
130+
.iconst(ir::types::I32, i64::from(VMGcKind::MASK));
131+
let actual_kind = builder.ins().band(kind_and_reserved_bits, kind_mask);
132+
133+
let expected_kind = builder
134+
.ins()
135+
.iconst(ir::types::I32, i64::from(expected_kind.as_u32()));
136+
137+
// NB: Do a subtype check rather than a strict equality check. See
138+
// `VMGcKind::matches` for details.
139+
let and = builder.ins().band(actual_kind, expected_kind);
140+
let matches = builder.ins().icmp(IntCC::Equal, and, expected_kind);
141+
142+
builder.ins().trapz(matches, TRAP_INTERNAL_ASSERT);
143+
}
144+
145+
/// Read a struct field or array element from its raw address in the GC heap.
99146
///
100147
/// The given address MUST have already been bounds-checked via
101148
/// `prepare_gc_ref_access`.
@@ -331,6 +378,8 @@ pub fn translate_struct_get(
331378
// type info from `wasmparser` and through to here is a bit funky.
332379
func_env.trapz(builder, struct_ref, crate::TRAP_NULL_REFERENCE);
333380

381+
emit_gc_kind_assert(func_env, builder, struct_ref, VMGcKind::StructRef);
382+
334383
let field_index = usize::try_from(field_index).unwrap();
335384
let interned_type_index = func_env.module.types[struct_type_index].unwrap_module_type_index();
336385

@@ -378,6 +427,8 @@ pub fn translate_struct_set(
378427
// TODO: See comment in `translate_struct_get` about the `trapz`.
379428
func_env.trapz(builder, struct_ref, crate::TRAP_NULL_REFERENCE);
380429

430+
emit_gc_kind_assert(func_env, builder, struct_ref, VMGcKind::StructRef);
431+
381432
let field_index = usize::try_from(field_index).unwrap();
382433
let interned_type_index = func_env.module.types[struct_type_index].unwrap_module_type_index();
383434

@@ -979,6 +1030,8 @@ pub fn translate_array_get(
9791030
) -> WasmResult<ir::Value> {
9801031
log::trace!("translate_array_get({array_type_index:?}, {array_ref:?}, {index:?})");
9811032

1033+
emit_gc_kind_assert(func_env, builder, array_ref, VMGcKind::ArrayRef);
1034+
9821035
let array_type_index = func_env.module.types[array_type_index].unwrap_module_type_index();
9831036
let elem_addr = array_elem_addr(func_env, builder, array_type_index, array_ref, index);
9841037

@@ -1000,6 +1053,8 @@ pub fn translate_array_set(
10001053
) -> WasmResult<()> {
10011054
log::trace!("translate_array_set({array_type_index:?}, {array_ref:?}, {index:?}, {value:?})");
10021055

1056+
emit_gc_kind_assert(func_env, builder, array_ref, VMGcKind::ArrayRef);
1057+
10031058
let array_type_index = func_env.module.types[array_type_index].unwrap_module_type_index();
10041059
let elem_addr = array_elem_addr(func_env, builder, array_type_index, array_ref, index);
10051060

crates/environ/src/gc.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -499,15 +499,8 @@ impl VMGcKind {
499499
#[inline]
500500
pub fn from_high_bits_of_u32(val: u32) -> VMGcKind {
501501
let masked = val & Self::MASK;
502-
let result = match masked {
503-
x if x == Self::ExternRef.as_u32() => Self::ExternRef,
504-
x if x == Self::AnyRef.as_u32() => Self::AnyRef,
505-
x if x == Self::EqRef.as_u32() => Self::EqRef,
506-
x if x == Self::ArrayRef.as_u32() => Self::ArrayRef,
507-
x if x == Self::StructRef.as_u32() => Self::StructRef,
508-
x if x == Self::ExnRef.as_u32() => Self::ExnRef,
509-
_ => panic!("invalid `VMGcKind`: {masked:#032b}"),
510-
};
502+
let result = Self::try_from_u32(masked)
503+
.unwrap_or_else(|| panic!("invalid `VMGcKind`: {masked:#032b}"));
511504

512505
let poison_kind = u32::from_le_bytes([POISON, POISON, POISON, POISON]) & VMGcKind::MASK;
513506
debug_assert_ne!(
@@ -531,6 +524,22 @@ impl VMGcKind {
531524
pub fn as_u32(self) -> u32 {
532525
self as u32
533526
}
527+
528+
/// Try to convert a `u32` into a `VMGcKind`.
529+
///
530+
/// Returns `None` if the value doesn't match any known kind.
531+
#[inline]
532+
pub fn try_from_u32(x: u32) -> Option<VMGcKind> {
533+
match x {
534+
_ if x == Self::ExternRef.as_u32() => Some(Self::ExternRef),
535+
_ if x == Self::AnyRef.as_u32() => Some(Self::AnyRef),
536+
_ if x == Self::EqRef.as_u32() => Some(Self::EqRef),
537+
_ if x == Self::ArrayRef.as_u32() => Some(Self::ArrayRef),
538+
_ if x == Self::StructRef.as_u32() => Some(Self::StructRef),
539+
_ if x == Self::ExnRef.as_u32() => Some(Self::ExnRef),
540+
_ => None,
541+
}
542+
}
534543
}
535544

536545
#[cfg(test)]

crates/wasmtime/src/runtime/store/gc.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,16 @@ impl StoreOpaque {
5656
bytes_needed: Option<u64>,
5757
asyncness: Asyncness,
5858
) {
59-
if let Some(n) = bytes_needed {
59+
if let Some(n) = bytes_needed
60+
// The gc_zeal's allocation counter will pass `bytes_needed == 0` to
61+
// signify that we shouldn't grow the GC heap, just do a collection.
62+
&& n > 0
63+
{
6064
if self.grow_gc_heap(limiter, n).await.is_ok() {
6165
return;
6266
}
6367
}
68+
6469
self.do_gc(asyncness).await;
6570
}
6671

crates/wasmtime/src/runtime/vm/gc.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,48 @@ pub struct GcStore {
4848

4949
/// The function-references table for this GC heap.
5050
pub func_ref_table: FuncRefTable,
51+
52+
/// An allocation counter that triggers GC when it reaches zero.
53+
///
54+
/// Initialized from the `WASMTIME_GC_ZEAL_ALLOC_COUNTER` environment
55+
/// variable. Decremented on every allocation and when it hits zero, a GC is
56+
/// forced and the counter is reset.
57+
#[cfg(all(gc_zeal, feature = "std"))]
58+
gc_zeal_alloc_counter: Option<NonZeroU32>,
59+
60+
/// The initial value to reset the counter to after it triggers.
61+
#[cfg(all(gc_zeal, feature = "std"))]
62+
gc_zeal_alloc_counter_init: Option<NonZeroU32>,
5163
}
5264

5365
impl GcStore {
5466
/// Create a new `GcStore`.
5567
pub fn new(allocation_index: GcHeapAllocationIndex, gc_heap: Box<dyn GcHeap>) -> Self {
5668
let host_data_table = ExternRefHostDataTable::default();
5769
let func_ref_table = FuncRefTable::default();
70+
71+
#[cfg(all(gc_zeal, feature = "std"))]
72+
let gc_zeal_alloc_counter_init =
73+
std::env::var("WASMTIME_GC_ZEAL_ALLOC_COUNTER")
74+
.ok()
75+
.map(|v| {
76+
v.parse::<NonZeroU32>().unwrap_or_else(|_| {
77+
panic!(
78+
"`WASMTIME_GC_ZEAL_ALLOC_COUNTER` must be a non-zero \
79+
`u32` value, got: {v}"
80+
)
81+
})
82+
});
83+
5884
Self {
5985
allocation_index,
6086
gc_heap,
6187
host_data_table,
6288
func_ref_table,
89+
#[cfg(all(gc_zeal, feature = "std"))]
90+
gc_zeal_alloc_counter: gc_zeal_alloc_counter_init,
91+
#[cfg(all(gc_zeal, feature = "std"))]
92+
gc_zeal_alloc_counter_init,
6393
}
6494
}
6595

@@ -231,6 +261,20 @@ impl GcStore {
231261
header: VMGcHeader,
232262
layout: Layout,
233263
) -> Result<Result<VMGcRef, u64>> {
264+
// When gc_zeal is enabled with an allocation counter, decrement it and
265+
// force a GC cycle when it reaches zero by returning a fake OOM.
266+
#[cfg(all(gc_zeal, feature = "std"))]
267+
if let Some(counter) = self.gc_zeal_alloc_counter.take() {
268+
match NonZeroU32::new(counter.get() - 1) {
269+
Some(c) => self.gc_zeal_alloc_counter = Some(c),
270+
None => {
271+
log::trace!("gc_zeal: allocation counter reached zero, forcing GC");
272+
self.gc_zeal_alloc_counter = self.gc_zeal_alloc_counter_init;
273+
return Ok(Err(0));
274+
}
275+
}
276+
}
277+
234278
self.gc_heap.alloc_raw(header, layout)
235279
}
236280

0 commit comments

Comments
 (0)