Skip to content

Commit c22b3cb

Browse files
authored
Reuse Wasm linear memories code for GC heaps (#10503)
* Reuse code for Wasm linear memories for GC heaps Instead of bespoke code paths and structures for Wasm GC, this commit makes it so that we now reuse VM structures like `VMMemoryDefinition` and bounds-checking logic. Notably, we also reuse all the associated bounds-checking optimizations and, when possible, virtual-memory techniques to completely elide them. Furthermore, this commit adds support for growing GC heaps, reusing the machinery for growing memories, and makes it so that GC heaps always start out empty. This allows us to properly delay allocating the GC heap's storage until a GC object is actually allocated. Fixes #9350 * fix c api compilation * use assert_contains * remove no-longer-necessary extra memory config from limiter tests * Helper for retry-after-maybe-async-gc in libcalls * Clean up some comments * fix wasmtime-fuzzing and no-gc compilation * fix examples * fix no-gc+compiler build * fix build without pooling allocator * fix +cranelift +gc-drc -gc-null builds * fix table hash key stability test * fix oracle usage of `ExternRef::new` * fix +gc -gc-null -gc-drc build * fix wasmtime-fuzzing * make `StorePtr` wrap a `NonNull` * Fix some doc tests * Remove some unnecessary retry helpers now that `FooRef::new` will auto-gc * fix things after rebase * Reorganize collection/growth methods for GC heap * rename BoundsCheck variants * fix cfg'ing of gc only code * Fix doc tests * fix one more gc cfg * disable GC heap OOM test on non-64-bit targets
1 parent bb12131 commit c22b3cb

File tree

776 files changed

+6352
-5560
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

776 files changed

+6352
-5560
lines changed

crates/c-api/src/store.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ pub extern "C" fn wasmtime_context_set_wasi(
221221

222222
#[unsafe(no_mangle)]
223223
pub extern "C" fn wasmtime_context_gc(mut context: WasmtimeStoreContextMut<'_>) {
224-
context.gc();
224+
context.gc(None);
225225
}
226226

227227
#[unsafe(no_mangle)]

crates/cranelift/src/bounds_checks.rs

Lines changed: 153 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
//! !!! !!!
2020
//! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
2121
22-
use super::Reachability;
23-
use crate::func_environ::FuncEnvironment;
24-
use crate::translate::{HeapData, TargetEnvironment};
22+
use crate::{
23+
func_environ::FuncEnvironment,
24+
translate::{HeapData, TargetEnvironment},
25+
Reachability,
26+
};
2527
use cranelift_codegen::{
2628
cursor::{Cursor, FuncCursor},
2729
ir::{self, condcodes::IntCC, InstBuilder, RelSourceLoc},
@@ -31,26 +33,155 @@ use cranelift_frontend::FunctionBuilder;
3133
use wasmtime_environ::Unsigned;
3234
use Reachability::*;
3335

36+
/// The kind of bounds check to perform when accessing a Wasm linear memory or
37+
/// GC heap.
38+
///
39+
/// Prefer `BoundsCheck::*WholeObject` over `BoundsCheck::Field` when possible,
40+
/// as that approach allows the mid-end to deduplicate bounds checks across
41+
/// multiple accesses to the same GC object.
42+
#[derive(Debug)]
43+
pub enum BoundsCheck {
44+
/// Check that this one access in particular is in bounds:
45+
///
46+
/// ```ignore
47+
/// index + offset + access_size <= bound
48+
/// ```
49+
StaticOffset { offset: u32, access_size: u8 },
50+
51+
/// Assuming the precondition `offset + access_size <= object_size`, check
52+
/// that this whole object is in bounds:
53+
///
54+
/// ```ignore
55+
/// index + object_size <= bound
56+
/// ```
57+
#[cfg(feature = "gc")]
58+
StaticObjectField {
59+
offset: u32,
60+
access_size: u8,
61+
object_size: u32,
62+
},
63+
64+
/// Like `StaticWholeObject` but with dynamic offset and object size.
65+
///
66+
/// It is *your* responsibility to ensure that the `offset + access_size <=
67+
/// object_size` precondition holds.
68+
#[cfg(feature = "gc")]
69+
DynamicObjectField {
70+
offset: ir::Value,
71+
object_size: ir::Value,
72+
},
73+
}
74+
3475
/// Helper used to emit bounds checks (as necessary) and compute the native
3576
/// address of a heap access.
3677
///
3778
/// Returns the `ir::Value` holding the native address of the heap access, or
38-
/// `None` if the heap access will unconditionally trap.
79+
/// `Reachability::Unreachable` if the heap access will unconditionally trap and
80+
/// any subsequent code in this basic block is unreachable.
3981
pub fn bounds_check_and_compute_addr(
4082
builder: &mut FunctionBuilder,
4183
env: &mut FuncEnvironment<'_>,
4284
heap: &HeapData,
43-
// Dynamic operand indexing into the heap.
4485
index: ir::Value,
45-
// Static immediate added to the index.
86+
bounds_check: BoundsCheck,
87+
trap: ir::TrapCode,
88+
) -> Reachability<ir::Value> {
89+
match bounds_check {
90+
BoundsCheck::StaticOffset {
91+
offset,
92+
access_size,
93+
} => bounds_check_field_access(builder, env, heap, index, offset, access_size, trap),
94+
95+
#[cfg(feature = "gc")]
96+
BoundsCheck::StaticObjectField {
97+
offset,
98+
access_size,
99+
object_size,
100+
} => {
101+
// Assert that the precondition holds.
102+
let offset_and_access_size = offset.checked_add(access_size.into()).unwrap();
103+
assert!(offset_and_access_size <= object_size);
104+
105+
// When we can, pretend that we are doing one big access of the
106+
// whole object all at once. This enables better GVN for repeated
107+
// accesses of the same object.
108+
if let Ok(object_size) = u8::try_from(object_size) {
109+
let obj_ptr = match bounds_check_field_access(
110+
builder,
111+
env,
112+
heap,
113+
index,
114+
0,
115+
object_size,
116+
trap,
117+
) {
118+
Reachable(v) => v,
119+
u @ Unreachable => return u,
120+
};
121+
let offset = builder.ins().iconst(env.pointer_type(), i64::from(offset));
122+
let field_ptr = builder.ins().iadd(obj_ptr, offset);
123+
return Reachable(field_ptr);
124+
}
125+
126+
// Otherwise, bounds check just this one field's access.
127+
bounds_check_field_access(builder, env, heap, index, offset, access_size, trap)
128+
}
129+
130+
// Compute the index of the end of the object, bounds check that and get
131+
// a pointer to just after the object, and then reverse offset from that
132+
// to get the pointer to the field being accessed.
133+
#[cfg(feature = "gc")]
134+
BoundsCheck::DynamicObjectField {
135+
offset,
136+
object_size,
137+
} => {
138+
assert_eq!(heap.index_type(), ir::types::I32);
139+
assert_eq!(builder.func.dfg.value_type(index), ir::types::I32);
140+
assert_eq!(builder.func.dfg.value_type(offset), ir::types::I32);
141+
assert_eq!(builder.func.dfg.value_type(object_size), ir::types::I32);
142+
143+
let index_and_object_size = builder.ins().uadd_overflow_trap(index, object_size, trap);
144+
let ptr_just_after_obj = match bounds_check_field_access(
145+
builder,
146+
env,
147+
heap,
148+
index_and_object_size,
149+
0,
150+
0,
151+
trap,
152+
) {
153+
Reachable(v) => v,
154+
u @ Unreachable => return u,
155+
};
156+
157+
let backwards_offset = builder.ins().isub(object_size, offset);
158+
let backwards_offset = cast_index_to_pointer_ty(
159+
backwards_offset,
160+
ir::types::I32,
161+
env.pointer_type(),
162+
false,
163+
&mut builder.cursor(),
164+
trap,
165+
);
166+
167+
let field_ptr = builder.ins().isub(ptr_just_after_obj, backwards_offset);
168+
Reachable(field_ptr)
169+
}
170+
}
171+
}
172+
173+
fn bounds_check_field_access(
174+
builder: &mut FunctionBuilder,
175+
env: &mut FuncEnvironment<'_>,
176+
heap: &HeapData,
177+
index: ir::Value,
46178
offset: u32,
47-
// Static size of the heap access.
48179
access_size: u8,
180+
trap: ir::TrapCode,
49181
) -> Reachability<ir::Value> {
50182
let pointer_bit_width = u16::try_from(env.pointer_type().bits()).unwrap();
51183
let bound_gv = heap.bound;
52184
let orig_index = index;
53-
let offset_and_size = offset_plus_size(offset, access_size);
54185
let clif_memory_traps_enabled = env.clif_memory_traps_enabled();
55186
let spectre_mitigations_enabled =
56187
env.heap_access_spectre_mitigation() && clif_memory_traps_enabled;
@@ -68,6 +199,7 @@ pub fn bounds_check_and_compute_addr(
68199
let memory_guard_size = env.tunables().memory_guard_size;
69200
let memory_reservation = env.tunables().memory_reservation;
70201

202+
let offset_and_size = offset_plus_size(offset, access_size);
71203
let statically_in_bounds = statically_in_bounds(&builder.func, heap, index, offset_and_size);
72204

73205
let index = cast_index_to_pointer_ty(
@@ -76,6 +208,7 @@ pub fn bounds_check_and_compute_addr(
76208
env.pointer_type(),
77209
heap.pcc_memory_type.is_some(),
78210
&mut builder.cursor(),
211+
trap,
79212
);
80213

81214
let oob_behavior = if spectre_mitigations_enabled {
@@ -166,7 +299,7 @@ pub fn bounds_check_and_compute_addr(
166299
// max_memory_size`, since we will end up being out-of-bounds regardless
167300
// of the given `index`.
168301
env.before_unconditionally_trapping_memory_access(builder);
169-
env.trap(builder, ir::TrapCode::HEAP_OUT_OF_BOUNDS);
302+
env.trap(builder, trap);
170303
return Unreachable;
171304
}
172305

@@ -176,7 +309,7 @@ pub fn bounds_check_and_compute_addr(
176309
// native pointer type anyway, so this is an unconditional trap.
177310
if pointer_bit_width < 64 && offset_and_size >= (1 << pointer_bit_width) {
178311
env.before_unconditionally_trapping_memory_access(builder);
179-
env.trap(builder, ir::TrapCode::HEAP_OUT_OF_BOUNDS);
312+
env.trap(builder, trap);
180313
return Unreachable;
181314
}
182315

@@ -297,6 +430,7 @@ pub fn bounds_check_and_compute_addr(
297430
oob_behavior,
298431
AddrPcc::static32(heap.pcc_memory_type, memory_reservation),
299432
oob,
433+
trap,
300434
));
301435
}
302436

@@ -330,6 +464,7 @@ pub fn bounds_check_and_compute_addr(
330464
oob_behavior,
331465
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
332466
oob,
467+
trap,
333468
));
334469
}
335470

@@ -378,6 +513,7 @@ pub fn bounds_check_and_compute_addr(
378513
oob_behavior,
379514
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
380515
oob,
516+
trap,
381517
));
382518
}
383519

@@ -422,6 +558,7 @@ pub fn bounds_check_and_compute_addr(
422558
oob_behavior,
423559
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
424560
oob,
561+
trap,
425562
));
426563
}
427564

@@ -439,12 +576,7 @@ pub fn bounds_check_and_compute_addr(
439576
builder.func.dfg.facts[access_size_val] =
440577
Some(Fact::constant(pointer_bit_width, offset_and_size));
441578
}
442-
let adjusted_index = env.uadd_overflow_trap(
443-
builder,
444-
index,
445-
access_size_val,
446-
ir::TrapCode::HEAP_OUT_OF_BOUNDS,
447-
);
579+
let adjusted_index = env.uadd_overflow_trap(builder, index, access_size_val, trap);
448580
if pcc {
449581
builder.func.dfg.facts[adjusted_index] = Some(Fact::value_offset(
450582
pointer_bit_width,
@@ -471,6 +603,7 @@ pub fn bounds_check_and_compute_addr(
471603
oob_behavior,
472604
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
473605
oob,
606+
trap,
474607
))
475608
}
476609

@@ -521,6 +654,7 @@ fn cast_index_to_pointer_ty(
521654
pointer_ty: ir::Type,
522655
pcc: bool,
523656
pos: &mut FuncCursor,
657+
trap: ir::TrapCode,
524658
) -> ir::Value {
525659
if index_ty == pointer_ty {
526660
return index;
@@ -544,8 +678,7 @@ fn cast_index_to_pointer_ty(
544678
let c32 = pos.ins().iconst(pointer_ty, 32);
545679
let high_bits = pos.ins().ushr(index, c32);
546680
let high_bits = pos.ins().ireduce(pointer_ty, high_bits);
547-
pos.ins()
548-
.trapnz(high_bits, ir::TrapCode::HEAP_OUT_OF_BOUNDS);
681+
pos.ins().trapnz(high_bits, trap);
549682
return low_bits;
550683
}
551684

@@ -623,9 +756,10 @@ fn explicit_check_oob_condition_and_compute_addr(
623756
// bounds (and therefore we should trap) and is zero when the heap access is
624757
// in bounds (and therefore we can proceed).
625758
oob_condition: ir::Value,
759+
trap: ir::TrapCode,
626760
) -> ir::Value {
627761
if let OobBehavior::ExplicitTrap = oob_behavior {
628-
env.trapnz(builder, oob_condition, ir::TrapCode::HEAP_OUT_OF_BOUNDS);
762+
env.trapnz(builder, oob_condition, trap);
629763
}
630764
let addr_ty = env.pointer_type();
631765

crates/cranelift/src/func_environ.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ pub struct FuncEnvironment<'module_environment> {
105105
wasmtime_environ::GcLayout,
106106
>,
107107

108+
#[cfg(feature = "gc")]
109+
gc_heap: Option<Heap>,
110+
111+
/// The Cranelift global holding the GC heap's base address.
112+
#[cfg(feature = "gc")]
113+
gc_heap_base: Option<ir::GlobalValue>,
114+
115+
/// The Cranelift global holding the GC heap's base address.
116+
#[cfg(feature = "gc")]
117+
gc_heap_bound: Option<ir::GlobalValue>,
118+
108119
#[cfg(feature = "wmemcheck")]
109120
translation: &'module_environment ModuleTranslation<'module_environment>,
110121

@@ -188,6 +199,12 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
188199

189200
#[cfg(feature = "gc")]
190201
ty_to_gc_layout: std::collections::HashMap::new(),
202+
#[cfg(feature = "gc")]
203+
gc_heap: None,
204+
#[cfg(feature = "gc")]
205+
gc_heap_base: None,
206+
#[cfg(feature = "gc")]
207+
gc_heap_bound: None,
191208

192209
heaps: PrimaryMap::default(),
193210
tables: SecondaryMap::default(),

0 commit comments

Comments
 (0)