Skip to content

Commit 2cf5e4a

Browse files
authored
Fix GC heap using wrong MemoryKind tunables in on-demand allocator (#13189)
* Fix GC heap using wrong `MemoryKind` tunables in on-demand allocator The on-demand instance allocator's `allocate_memory` always passed `MemoryKind::LinearMemory` to `Memory::new_dynamic`, even when allocating the backing memory for a GC heap. This caused the GC heap to use the wrong set of tunables. The consequences were two distinct crash modes: 1. When `memory_reservation=0`: the GC heap got a 0-byte reservation, so every growth triggered an mmap reallocation that changed the base pointer. Code held a stale cached copy of the old base pointer, causing use-after-free crashes. 2. When `gc_heap_guard_size != memory_guard_size`: Cranelift correctly compiled GC heap accesses using `gc_heap_guard_size` bytes of virtual guard, but the runtime memory incorrectly only had `memory_guard_size` bytes of actual guard, so accesses beyond the actual guard caused uncaught segfaults. This commit fixes the bug by threading a `MemoryKind` through the `InstanceAllocator::allocate_memory` trait method. Also, now that the GC heap correctly uses `gc_heap_reservation`, the workaround in `MemoryTunables::may_move` that always returned `true` for GC heaps is no longer needed. That workaround was masking the actual root cause, and with the fix, `gc_heap_may_move` can now be respected correctly. There were three existing tests that were accidentally relying on the GC heap using linear-memory tunables; this commit adds explicit GC heap tunable configuration so they continue to work correctly. Fixes #13173 * Fix compiler warning
1 parent e5cdeb4 commit 2cf5e4a

8 files changed

Lines changed: 151 additions & 18 deletions

File tree

crates/environ/src/tunables.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -392,13 +392,7 @@ impl<'a> MemoryTunables<'a> {
392392
pub fn may_move(&self) -> bool {
393393
match self.kind {
394394
MemoryKind::LinearMemory => self.tunables.memory_may_move,
395-
MemoryKind::GcHeap => {
396-
// XXX: Lazy GC heap allocation means that the GC heap's base
397-
// may always "move" in that it is initially null and then gets
398-
// initialized after being lazily allocated.
399-
let _ = self.tunables.gc_heap_may_move;
400-
true
401-
}
395+
MemoryKind::GcHeap => self.tunables.gc_heap_may_move,
402396
}
403397
}
404398

crates/wasmtime/src/runtime/store.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1969,7 +1969,12 @@ impl StoreOpaque {
19691969

19701970
let (mem_alloc_index, mem) = engine
19711971
.allocator()
1972-
.allocate_memory(&mut request, &mem_ty, None)
1972+
.allocate_memory(
1973+
&mut request,
1974+
&mem_ty,
1975+
None,
1976+
wasmtime_environ::MemoryKind::GcHeap,
1977+
)
19731978
.await?;
19741979

19751980
// Then, allocate the actual GC heap, passing in that memory

crates/wasmtime/src/runtime/trampoline/memory.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use alloc::sync::Arc;
1212
use core::future::Future;
1313
use core::pin::Pin;
1414
use wasmtime_environ::{
15-
DefinedMemoryIndex, DefinedTableIndex, EntityIndex, HostPtr, MemoryTunables, Module,
16-
StaticModuleIndex, VMOffsets,
15+
DefinedMemoryIndex, DefinedTableIndex, EntityIndex, HostPtr, MemoryKind, MemoryTunables,
16+
Module, StaticModuleIndex, VMOffsets,
1717
};
1818

1919
#[cfg(feature = "component-model")]
@@ -176,6 +176,7 @@ unsafe impl InstanceAllocator for SingleMemoryInstance<'_> {
176176
request: &'a mut InstanceAllocationRequest<'b, 'c>,
177177
ty: &'a wasmtime_environ::Memory,
178178
memory_index: Option<DefinedMemoryIndex>,
179+
memory_kind: MemoryKind,
179180
) -> Pin<Box<dyn Future<Output = Result<(MemoryAllocationIndex, Memory)>> + Send + 'a>> {
180181
if cfg!(debug_assertions) {
181182
let module = request.runtime_info.env_module();
@@ -191,7 +192,9 @@ unsafe impl InstanceAllocator for SingleMemoryInstance<'_> {
191192
shared_memory.clone().as_memory(),
192193
))
193194
}),
194-
None => self.ondemand.allocate_memory(request, ty, memory_index),
195+
None => self
196+
.ondemand
197+
.allocate_memory(request, ty, memory_index, memory_kind),
195198
}
196199
}
197200

crates/wasmtime/src/runtime/vm/instance/allocator.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use core::pin::Pin;
1414
use core::{mem, ptr};
1515
use wasmtime_environ::{
1616
DefinedMemoryIndex, DefinedTableIndex, EntityRef, HostPtr, InitMemory, MemoryInitialization,
17-
MemoryInitializer, Module, SizeOverflow, TableInitialValue, TableSegmentElements, Trap,
18-
VMOffsets, WasmRefType,
17+
MemoryInitializer, MemoryKind, Module, SizeOverflow, TableInitialValue, TableSegmentElements,
18+
Trap, VMOffsets, WasmRefType,
1919
};
2020

2121
#[cfg(feature = "gc")]
@@ -195,6 +195,7 @@ pub unsafe trait InstanceAllocator: Send + Sync {
195195
request: &'a mut InstanceAllocationRequest<'b, 'c>,
196196
ty: &'a wasmtime_environ::Memory,
197197
memory_index: Option<DefinedMemoryIndex>,
198+
memory_kind: MemoryKind,
198199
) -> Pin<Box<dyn Future<Output = Result<(MemoryAllocationIndex, Memory)>> + Send + 'a>>;
199200

200201
/// Deallocate an instance's previously allocated memory.
@@ -418,7 +419,7 @@ impl dyn InstanceAllocator + '_ {
418419
.expect("should be a defined memory since we skipped imported ones");
419420

420421
let memory = self
421-
.allocate_memory(request, ty, Some(memory_index))
422+
.allocate_memory(request, ty, Some(memory_index), MemoryKind::LinearMemory)
422423
.await?;
423424
memories.push(memory)?;
424425
}

crates/wasmtime/src/runtime/vm/instance/allocator/on_demand.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use crate::runtime::vm::table::Table;
1010
use alloc::sync::Arc;
1111
use core::future::Future;
1212
use core::pin::Pin;
13-
use wasmtime_environ::{DefinedMemoryIndex, DefinedTableIndex, HostPtr, Module, VMOffsets};
13+
use wasmtime_environ::{
14+
DefinedMemoryIndex, DefinedTableIndex, HostPtr, MemoryKind, Module, VMOffsets,
15+
};
1416

1517
#[cfg(feature = "gc")]
1618
use crate::runtime::vm::{GcHeap, GcHeapAllocationIndex, GcRuntime};
@@ -115,7 +117,10 @@ unsafe impl InstanceAllocator for OnDemandInstanceAllocator {
115117
request: &'a mut InstanceAllocationRequest<'b, 'c>,
116118
ty: &'a wasmtime_environ::Memory,
117119
memory_index: Option<DefinedMemoryIndex>,
120+
memory_kind: MemoryKind,
118121
) -> Pin<Box<dyn Future<Output = Result<(MemoryAllocationIndex, Memory)>> + Send + 'a>> {
122+
debug_assert_eq!(memory_index.is_none(), memory_kind == MemoryKind::GcHeap);
123+
119124
let creator = self
120125
.mem_creator
121126
.as_deref()
@@ -135,6 +140,7 @@ unsafe impl InstanceAllocator for OnDemandInstanceAllocator {
135140
creator,
136141
image,
137142
request.limiter.as_deref_mut(),
143+
memory_kind,
138144
)
139145
.await?;
140146
Ok((allocation_index, memory))

crates/wasmtime/src/runtime/vm/instance/allocator/pooling.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ use std::{
6666
sync::atomic::{AtomicU64, Ordering},
6767
};
6868
use wasmtime_environ::{
69-
DefinedMemoryIndex, DefinedTableIndex, HostPtr, Module, Tunables, VMOffsets,
69+
DefinedMemoryIndex, DefinedTableIndex, HostPtr, MemoryKind, Module, Tunables, VMOffsets,
7070
};
7171

7272
pub use self::metrics::PoolingAllocatorMetrics;
@@ -688,6 +688,7 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator {
688688
request: &'a mut InstanceAllocationRequest<'b, 'c>,
689689
ty: &'a wasmtime_environ::Memory,
690690
memory_index: Option<DefinedMemoryIndex>,
691+
_memory_kind: MemoryKind,
691692
) -> Pin<Box<dyn Future<Output = Result<(MemoryAllocationIndex, Memory)>> + Send + 'a>> {
692693
crate::runtime::box_future(async move {
693694
async {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,11 @@ impl Memory {
244244
creator: &dyn RuntimeMemoryCreator,
245245
memory_image: Option<&Arc<MemoryImage>>,
246246
limiter: Option<&mut StoreResourceLimiter<'_>>,
247+
kind: MemoryKind,
247248
) -> Result<Self> {
248249
let (minimum, maximum) = Self::limit_new(ty, limiter).await?;
249250
let tunables = engine.tunables();
250-
let memory_tunables = MemoryTunables::new(tunables, MemoryKind::LinearMemory);
251+
let memory_tunables = MemoryTunables::new(tunables, kind);
251252
let allocation = creator.new_memory(ty, &memory_tunables, minimum, maximum)?;
252253

253254
let memory = LocalMemory::new(ty, &memory_tunables, allocation, memory_image)?;

tests/all/gc.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1581,6 +1581,9 @@ fn instantiate_global_init_oom() -> Result<()> {
15811581
config.memory_may_move(false);
15821582
config.memory_reservation(64 << 10);
15831583
config.memory_reservation_for_growth(0);
1584+
config.gc_heap_may_move(false);
1585+
config.gc_heap_reservation(64 << 10);
1586+
config.gc_heap_reservation_for_growth(0);
15841587
let engine = Engine::new(&config)?;
15851588
let mut store = Store::new(&engine, ());
15861589

@@ -1611,6 +1614,9 @@ fn elem_const_eval_oom() -> Result<()> {
16111614
config.memory_may_move(false);
16121615
config.memory_reservation(64 << 10);
16131616
config.memory_reservation_for_growth(0);
1617+
config.gc_heap_may_move(false);
1618+
config.gc_heap_reservation(64 << 10);
1619+
config.gc_heap_reservation_for_growth(0);
16141620
let engine = Engine::new(&config)?;
16151621
let mut store = Store::new(&engine, ());
16161622

@@ -1845,7 +1851,7 @@ fn issue_13141_gc_heap_may_not_move() -> Result<()> {
18451851
let mut config = Config::new();
18461852
config.wasm_gc(true);
18471853
config.gc_heap_may_move(false);
1848-
config.gc_heap_reservation(0);
1854+
18491855
let engine = Engine::new(&config)?;
18501856

18511857
let module = Module::new(
@@ -2023,3 +2029,119 @@ fn issue_13037_drc_leak_passing_objects_already_in_over_approx_stack_roots_list(
20232029

20242030
Ok(())
20252031
}
2032+
2033+
#[test]
2034+
#[cfg_attr(miri, ignore)]
2035+
fn issue_13173_gc_heap_uses_gc_tunables_no_signals() -> Result<()> {
2036+
let _ = env_logger::try_init();
2037+
2038+
let mut config = Config::new();
2039+
config.wasm_gc(true);
2040+
config.wasm_function_references(true);
2041+
config.collector(Collector::DeferredReferenceCounting);
2042+
// Set `memory_reservation=0` while leaving `gc_heap_reservation` at its
2043+
// default (4GB on 64-bit). Before the fix, the GC heap would incorrectly
2044+
// use `memory_reservation=0`, giving it 0 capacity and causing segfaults
2045+
// as the heap base pointer changed on every growth.
2046+
config.memory_reservation(0);
2047+
config.signals_based_traps(false);
2048+
2049+
let engine = Engine::new(&config)?;
2050+
2051+
let module = Module::new(
2052+
&engine,
2053+
r#"
2054+
(module
2055+
(type $arr (array (mut i32)))
2056+
(func (export "run") (result i32)
2057+
(local $i i32)
2058+
(local $a (ref $arr))
2059+
(local.set $a (array.new_default $arr (i32.const 64)))
2060+
(block $break
2061+
(loop $loop
2062+
(br_if $break (i32.ge_u (local.get $i) (i32.const 1000)))
2063+
(array.set $arr
2064+
(local.get $a)
2065+
(i32.const 0)
2066+
(i32.add
2067+
(array.get $arr (local.get $a) (i32.const 0))
2068+
(i32.const 1)))
2069+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
2070+
(br $loop)
2071+
)
2072+
)
2073+
(array.get $arr (local.get $a) (i32.const 0))
2074+
)
2075+
)
2076+
"#,
2077+
)?;
2078+
2079+
let mut store = Store::new(&engine, ());
2080+
let instance = Instance::new(&mut store, &module, &[])?;
2081+
let run = instance.get_typed_func::<(), i32>(&mut store, "run")?;
2082+
let result = run.call(&mut store, ())?;
2083+
assert_eq!(result, 1000);
2084+
Ok(())
2085+
}
2086+
2087+
#[test]
2088+
#[cfg_attr(miri, ignore)]
2089+
fn issue_13173_gc_heap_uses_gc_tunables_guard_size_mismatch() -> Result<()> {
2090+
if std::mem::size_of::<usize>() < std::mem::size_of::<u64>()
2091+
|| std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok()
2092+
{
2093+
return Ok(());
2094+
}
2095+
2096+
let _ = env_logger::try_init();
2097+
2098+
let mut config = Config::new();
2099+
config.wasm_gc(true);
2100+
config.wasm_function_references(true);
2101+
config.collector(Collector::DeferredReferenceCounting);
2102+
// Set `memory_reservation=0` and a large `gc_heap_guard_size` while leaving
2103+
// `memory_guard_size` at its default. Before the fix, the GC heap would use
2104+
// `memory_guard_size` (small) instead of `gc_heap_guard_size` (large), causing
2105+
// Cranelift-generated code that relies on virtual memory protection for
2106+
// accesses within the gc_heap_guard_size to segfault.
2107+
config.memory_reservation(0);
2108+
config.gc_heap_guard_size(1 << 29); // 512MB GC heap guard
2109+
config.memory_guard_size(32 * 1024 * 1024); // 32MB linear memory guard
2110+
2111+
let engine = Engine::new(&config)?;
2112+
2113+
let module = Module::new(
2114+
&engine,
2115+
r#"
2116+
(module
2117+
(type $arr (array (mut i32)))
2118+
(func (export "run") (result i32)
2119+
(local $i i32)
2120+
(local $a (ref $arr))
2121+
(local.set $a (array.new_default $arr (i32.const 64)))
2122+
(block $break
2123+
(loop $loop
2124+
(br_if $break (i32.ge_u (local.get $i) (i32.const 1000)))
2125+
(array.set $arr
2126+
(local.get $a)
2127+
(i32.const 0)
2128+
(i32.add
2129+
(array.get $arr (local.get $a) (i32.const 0))
2130+
(i32.const 1)))
2131+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
2132+
(br $loop)
2133+
)
2134+
)
2135+
(array.get $arr (local.get $a) (i32.const 0))
2136+
)
2137+
)
2138+
"#,
2139+
)?;
2140+
2141+
let mut store = Store::new(&engine, ());
2142+
let instance = Instance::new(&mut store, &module, &[])?;
2143+
let run = instance.get_typed_func::<(), i32>(&mut store, "run")?;
2144+
let result = run.call(&mut store, ())?;
2145+
assert_eq!(result, 1000);
2146+
Ok(())
2147+
}

0 commit comments

Comments
 (0)