Skip to content

Commit 18a8ad6

Browse files
authored
Fix corrupted pooling allocator state on OOM (#13579)
This commit fixes an issue where if the pooling allocator generated OOM at just the right time then it would leave the pooling allocator in a corrupt state where the instance count was incremented, never decremented. This additionally fixes a separate issue where if an OOM happened just a bit later then it would mean that tables/memories from the pooling allocator would leak and not be properly destroyed within the allocator, leaking their slots.
1 parent 40ac363 commit 18a8ad6

3 files changed

Lines changed: 65 additions & 20 deletions

File tree

crates/fuzzing/tests/oom/instance.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#![cfg(arc_try_new)]
22

33
use wasmtime::{
4-
Config, Engine, Func, Global, GlobalType, Instance, Linker, Module, Mutability, Result, Store,
5-
Val, ValType,
4+
Config, Engine, Func, Global, GlobalType, Instance, InstanceAllocationStrategy, Linker, Module,
5+
Mutability, PoolingAllocationConfig, Result, Store, Val, ValType,
66
};
77
use wasmtime_fuzzing::oom::OomTest;
88

@@ -326,6 +326,32 @@ fn instance_global_init_with_imported_global() -> Result<()> {
326326
})
327327
}
328328

329+
#[test]
330+
fn pooling_allocator_memory_slot_leak_on_oom() -> Result<()> {
331+
let mut pool = PoolingAllocationConfig::default();
332+
pool.total_memories(1);
333+
pool.total_tables(10);
334+
pool.total_core_instances(10);
335+
336+
let mut config = Config::new();
337+
config.concurrency_support(false);
338+
config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool));
339+
let engine = Engine::new(&config)?;
340+
341+
let module = Module::new(&engine, r#"(module (memory (export "m") 1))"#)?;
342+
let linker = Linker::<()>::new(&engine);
343+
let instance_pre = linker.instantiate_pre(&module)?;
344+
345+
// With only one memory slot in the pooling allocator shared amongst all
346+
// runs here we should be guaranteed that an OOM failure anywhere always
347+
// returns the memory, if any, back to the pooling allocator.
348+
OomTest::new().allow_alloc_after_oom(true).test(|| {
349+
let mut store = Store::try_new(&engine, ())?;
350+
let _instance = instance_pre.instantiate(&mut store)?;
351+
Ok(())
352+
})
353+
}
354+
329355
#[test]
330356
fn table_element_segment_init() -> Result<()> {
331357
let module_bytes = {

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,14 @@ pub struct Instance {
151151
}
152152

153153
impl Instance {
154-
/// Create an instance at the given memory address.
154+
/// Creates a new owned instance handle from `req`.
155155
///
156-
/// It is assumed the memory was properly aligned and the
157-
/// allocation was `alloc_size` in bytes.
156+
/// The runtime memory/table data structures must have been previously
157+
/// allocated and are present within `memories` and `tables`. These values
158+
/// are `mem::take`n upon allocation success of an instance, and if the
159+
/// instance allocation fails then these are otherwise left in place. This
160+
/// enable the pooling allocator to run custom deallocation code for them,
161+
/// for example.
158162
///
159163
/// # Safety
160164
///
@@ -163,8 +167,8 @@ impl Instance {
163167
/// and `tables` must have been allocated for `req.store`.
164168
unsafe fn new(
165169
req: InstanceAllocationRequest,
166-
memories: TryPrimaryMap<DefinedMemoryIndex, (MemoryAllocationIndex, Memory)>,
167-
tables: TryPrimaryMap<DefinedTableIndex, (TableAllocationIndex, Table)>,
170+
memories: &mut TryPrimaryMap<DefinedMemoryIndex, (MemoryAllocationIndex, Memory)>,
171+
tables: &mut TryPrimaryMap<DefinedTableIndex, (TableAllocationIndex, Table)>,
168172
) -> Result<InstanceHandle, OutOfMemory> {
169173
let module = req.runtime_info.env_module();
170174
let memory_tys = &module.memories;
@@ -191,18 +195,28 @@ impl Instance {
191195
passive_elements.push(PassiveElementSegment::new(*ty, len)?)?;
192196
}
193197

198+
// Allocate the instance and its `VMContext` with empty memory and table
199+
// maps. This is the final fallible allocation in this function; only
200+
// after it succeeds do we transfer ownership of the pool-allocated
201+
// `memories`/`tables` into the instance (below), so that a failure here
202+
// leaves them in the caller's deallocation guard to be freed.
194203
let mut ret = OwnedInstance::new(Instance {
195204
id: req.id,
196205
runtime_info: req.runtime_info.clone(),
197-
memories,
198-
tables,
206+
memories: TryPrimaryMap::default(),
207+
tables: TryPrimaryMap::default(),
199208
passive_elements,
200209
#[cfg(feature = "wmemcheck")]
201210
wmemcheck_state,
202211
store: None,
203212
vmctx: OwnedVMContext::new(),
204213
})?;
205214

215+
// Can't fail any more, so transfer ownership of `memories` and `tables`
216+
// to this instance.
217+
*ret.get_mut().memories_mut() = mem::take(memories);
218+
*ret.get_mut().tables_mut() = mem::take(tables);
219+
206220
// SAFETY: this vmctx was allocated with the same layout above, so it
207221
// should be safe to initialize with the same values here.
208222
unsafe {

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -324,32 +324,35 @@ impl dyn InstanceAllocator + '_ {
324324
.expect("module should have already been validated before allocation");
325325
}
326326

327-
self.increment_core_instance_count()?;
328-
329327
let num_defined_memories = module.num_defined_memories();
330328
let num_defined_tables = module.num_defined_tables();
331329

330+
let memories = TryPrimaryMap::with_capacity(num_defined_memories)?;
331+
let tables = TryPrimaryMap::with_capacity(num_defined_tables)?;
332+
333+
// Note that incrementing the instance count here must be done just
334+
// before creation of `DeallocateOnDrop`. This is required to ensure
335+
// that upon successful increment it'll get paired with a decrement
336+
// below should anything fail.
337+
self.increment_core_instance_count()?;
332338
let mut guard = DeallocateOnDrop {
333339
run_deallocate: true,
334-
memories: TryPrimaryMap::with_capacity(num_defined_memories)?,
335-
tables: TryPrimaryMap::with_capacity(num_defined_tables)?,
340+
memories,
341+
tables,
336342
allocator: self,
343+
// NB: do not add more initialization here if it can fail, move that
344+
// above the increment above instead.
337345
};
338346

339347
self.allocate_memories(&mut request, &mut guard.memories)
340348
.await?;
341349
self.allocate_tables(&mut request, &mut guard.tables)
342350
.await?;
351+
343352
// SAFETY: memories/tables were just allocated from the store within
344353
// `request` and this function's own contract requires that the
345354
// imports are valid.
346-
let handle = unsafe {
347-
Instance::new(
348-
request,
349-
mem::take(&mut guard.memories),
350-
mem::take(&mut guard.tables),
351-
)?
352-
};
355+
let handle = unsafe { Instance::new(request, &mut guard.memories, &mut guard.tables)? };
353356
guard.run_deallocate = false;
354357
return Ok(handle);
355358

@@ -363,6 +366,8 @@ impl dyn InstanceAllocator + '_ {
363366
impl Drop for DeallocateOnDrop<'_> {
364367
fn drop(&mut self) {
365368
if !self.run_deallocate {
369+
debug_assert!(self.memories.is_empty());
370+
debug_assert!(self.tables.is_empty());
366371
return;
367372
}
368373
// SAFETY: these were previously allocated by this allocator

0 commit comments

Comments
 (0)