use super::{
MemoryAllocationIndex,
index_allocator::{MemoryInModule, ModuleAffinityIndexAllocator, SlotId},
};
use crate::prelude::*;
use crate::runtime::vm::{
CompiledModuleId, InstanceAllocationRequest, InstanceLimits, Memory, MemoryBase,
MemoryImageSlot, Mmap, MmapOffset, PoolingInstanceAllocatorConfig, mmap::AlignedLength,
};
use crate::{
Enabled,
runtime::vm::mpk::{self, ProtectionKey, ProtectionMask},
vm::HostAlignedByteCount,
};
use std::mem;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use wasmtime_environ::{DefinedMemoryIndex, Module, Tunables};
#[derive(Debug)]
struct Stripe {
allocator: ModuleAffinityIndexAllocator,
pkey: Option<ProtectionKey>,
}
#[derive(Debug)]
pub struct MemoryPool {
mapping: Arc<Mmap<AlignedLength>>,
stripes: Vec<Stripe>,
image_slots: Vec<Mutex<ImageSlot>>,
layout: SlabLayout,
memories_per_instance: usize,
pub(super) keep_resident: HostAlignedByteCount,
next_available_pkey: AtomicUsize,
}
#[derive(Debug)]
enum ImageSlot {
Unmapped,
Unknown,
PreviouslyUsed(MemoryImageSlot),
}
impl MemoryPool {
pub fn new(config: &PoolingInstanceAllocatorConfig, tunables: &Tunables) -> Result<Self> {
if u64::try_from(config.limits.max_memory_size).unwrap() > tunables.memory_reservation {
bail!(
"maximum memory size of {:#x} bytes exceeds the configured \
memory reservation of {:#x} bytes",
config.limits.max_memory_size,
tunables.memory_reservation
);
}
let pkeys = match config.memory_protection_keys {
Enabled::Auto => {
if mpk::is_supported() {
mpk::keys(config.max_memory_protection_keys)
} else {
&[]
}
}
Enabled::Yes => {
if mpk::is_supported() {
mpk::keys(config.max_memory_protection_keys)
} else {
bail!("mpk is disabled on this system")
}
}
Enabled::No => &[],
};
if !pkeys.is_empty() {
mpk::allow(ProtectionMask::all());
}
let constraints = SlabConstraints::new(&config.limits, tunables, pkeys.len())?;
let layout = calculate(&constraints)?;
log::debug!(
"creating memory pool: {constraints:?} -> {layout:?} (total: {})",
layout.total_slab_bytes()?
);
let mut mapping =
Mmap::accessible_reserved(HostAlignedByteCount::ZERO, layout.total_slab_bytes()?)
.context("failed to create memory pool mapping")?;
if layout.num_stripes >= 2 {
let mut cursor = layout.pre_slab_guard_bytes;
let pkeys = &pkeys[..layout.num_stripes];
for i in 0..constraints.num_slots {
let pkey = &pkeys[i % pkeys.len()];
let region = unsafe {
mapping.slice_mut(
cursor.byte_count()..cursor.byte_count() + layout.slot_bytes.byte_count(),
)
};
pkey.protect(region)?;
cursor = cursor
.checked_add(layout.slot_bytes)
.context("cursor + slot_bytes overflows")?;
}
debug_assert_eq!(
cursor
.checked_add(layout.post_slab_guard_bytes)
.context("cursor + post_slab_guard_bytes overflows")?,
layout.total_slab_bytes()?
);
}
let image_slots: Vec<_> = std::iter::repeat_with(|| Mutex::new(ImageSlot::Unmapped))
.take(constraints.num_slots)
.collect();
let create_stripe = |i| {
let num_slots = constraints.num_slots / layout.num_stripes
+ usize::from(constraints.num_slots % layout.num_stripes > i);
let allocator = ModuleAffinityIndexAllocator::new(
num_slots.try_into().unwrap(),
config.max_unused_warm_slots,
);
Stripe {
allocator,
pkey: pkeys.get(i).cloned(),
}
};
debug_assert!(layout.num_stripes > 0);
let stripes: Vec<_> = (0..layout.num_stripes).map(create_stripe).collect();
let pool = Self {
stripes,
mapping: Arc::new(mapping),
image_slots,
layout,
memories_per_instance: usize::try_from(config.limits.max_memories_per_module).unwrap(),
keep_resident: HostAlignedByteCount::new_rounded_up(
config.linear_memory_keep_resident,
)?,
next_available_pkey: AtomicUsize::new(0),
};
Ok(pool)
}
pub fn next_available_pkey(&self) -> Option<ProtectionKey> {
let index = self.next_available_pkey.fetch_add(1, Ordering::SeqCst) % self.stripes.len();
debug_assert!(
self.stripes.len() < 2 || self.stripes[index].pkey.is_some(),
"if we are using stripes, we cannot have an empty protection key"
);
self.stripes[index].pkey
}
pub fn validate_memories(&self, module: &Module) -> Result<()> {
let memories = module.num_defined_memories();
if memories > self.memories_per_instance {
bail!(
"defined memories count of {} exceeds the per-instance limit of {}",
memories,
self.memories_per_instance,
);
}
for (i, memory) in module.memories.iter().skip(module.num_imported_memories) {
self.validate_memory(memory).with_context(|| {
format!(
"memory index {} is unsupported in this pooling allocator configuration",
i.as_u32()
)
})?;
}
Ok(())
}
pub fn validate_memory(&self, memory: &wasmtime_environ::Memory) -> Result<()> {
let min = memory.minimum_byte_size().with_context(|| {
format!("memory has a minimum byte size that cannot be represented in a u64",)
})?;
if min > u64::try_from(self.layout.max_memory_bytes.byte_count()).unwrap() {
bail!(
"memory has a minimum byte size of {} which exceeds the limit of {} bytes",
min,
self.layout.max_memory_bytes,
);
}
if memory.shared {
bail!("memory is shared which is not supported in the pooling allocator");
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.stripes.iter().all(|s| s.allocator.is_empty())
}
pub async fn allocate(
&self,
request: &mut InstanceAllocationRequest<'_, '_>,
ty: &wasmtime_environ::Memory,
memory_index: Option<DefinedMemoryIndex>,
) -> Result<(MemoryAllocationIndex, Memory)> {
let tunables = request.store.engine().tunables();
let stripe_index = if let Some(pkey) = request.store.get_pkey() {
pkey.as_stripe()
} else {
debug_assert!(self.stripes.len() < 2);
0
};
let striped_allocation_index = self.stripes[stripe_index]
.allocator
.alloc(memory_index.and_then(|mem_idx| {
request
.runtime_info
.unique_id()
.map(|id| MemoryInModule(id, mem_idx))
}))
.map(|slot| StripedAllocationIndex(u32::try_from(slot.index()).unwrap()))
.ok_or_else(|| {
super::PoolConcurrencyLimitError::new(
self.stripes[stripe_index].allocator.len(),
format!("memory stripe {stripe_index}"),
)
})?;
let mut guard = DeallocateIndexGuard {
pool: self,
stripe_index,
striped_allocation_index,
active: true,
};
let allocation_index =
striped_allocation_index.as_unstriped_slot_index(stripe_index, self.stripes.len());
assert!(
tunables.memory_reservation + tunables.memory_guard_size
<= u64::try_from(self.layout.bytes_to_next_stripe_slot().byte_count()).unwrap()
);
let base = self.get_base(allocation_index);
let base_capacity = self.layout.max_memory_bytes;
let mut slot = self.take_memory_image_slot(allocation_index)?;
let image = match memory_index {
Some(memory_index) => request.runtime_info.memory_image(memory_index)?,
None => None,
};
let initial_size = ty
.minimum_byte_size()
.expect("min size checked in validation");
let initial_size = usize::try_from(initial_size).unwrap();
slot.instantiate(initial_size, image, ty, tunables)?;
let memory = Memory::new_static(
ty,
tunables,
MemoryBase::Mmap(base),
base_capacity.byte_count(),
slot,
request.limiter.as_deref_mut(),
)
.await?;
guard.active = false;
return Ok((allocation_index, memory));
struct DeallocateIndexGuard<'a> {
pool: &'a MemoryPool,
stripe_index: usize,
striped_allocation_index: StripedAllocationIndex,
active: bool,
}
impl Drop for DeallocateIndexGuard<'_> {
fn drop(&mut self) {
if !self.active {
return;
}
self.pool.stripes[self.stripe_index]
.allocator
.free(SlotId(self.striped_allocation_index.0), 0);
}
}
}
pub unsafe fn deallocate(
&self,
allocation_index: MemoryAllocationIndex,
image: Option<MemoryImageSlot>,
bytes_resident: usize,
) {
self.return_memory_image_slot(allocation_index, image);
let (stripe_index, striped_allocation_index) =
StripedAllocationIndex::from_unstriped_slot_index(allocation_index, self.stripes.len());
self.stripes[stripe_index]
.allocator
.free(SlotId(striped_allocation_index.0), bytes_resident);
}
pub fn purge_module(&self, module: CompiledModuleId) {
for (stripe_index, stripe) in self.stripes.iter().enumerate() {
for i in 0..self.memories_per_instance {
use wasmtime_environ::EntityRef;
let memory_index = DefinedMemoryIndex::new(i);
while let Some(id) = stripe
.allocator
.alloc_affine_and_clear_affinity(module, memory_index)
{
let index = StripedAllocationIndex(id.0)
.as_unstriped_slot_index(stripe_index, self.stripes.len());
if let Ok(mut slot) = self.take_memory_image_slot(index) {
if slot.remove_image().is_ok() {
self.return_memory_image_slot(index, Some(slot));
}
}
stripe.allocator.free(id, 0);
}
}
}
}
fn get_base(&self, allocation_index: MemoryAllocationIndex) -> MmapOffset {
assert!(allocation_index.index() < self.layout.num_slots);
let offset = self
.layout
.slot_bytes
.checked_mul(allocation_index.index())
.and_then(|c| c.checked_add(self.layout.pre_slab_guard_bytes))
.expect("slot_bytes * index + pre_slab_guard_bytes overflows");
self.mapping.offset(offset).expect("offset is in bounds")
}
fn take_memory_image_slot(
&self,
allocation_index: MemoryAllocationIndex,
) -> Result<MemoryImageSlot> {
let (maybe_slot, needs_reset) = {
let mut slot = self.image_slots[allocation_index.index()].lock().unwrap();
match mem::replace(&mut *slot, ImageSlot::Unknown) {
ImageSlot::Unmapped => (None, false),
ImageSlot::Unknown => (None, true),
ImageSlot::PreviouslyUsed(state) => (Some(state), false),
}
};
let mut slot = maybe_slot.unwrap_or_else(|| {
MemoryImageSlot::create(
self.get_base(allocation_index),
HostAlignedByteCount::ZERO,
self.layout.max_memory_bytes.byte_count(),
)
});
if needs_reset {
slot.reset_with_anon_memory()?;
}
Ok(slot)
}
fn return_memory_image_slot(
&self,
allocation_index: MemoryAllocationIndex,
slot: Option<MemoryImageSlot>,
) {
let prev = mem::replace(
&mut *self.image_slots[allocation_index.index()].lock().unwrap(),
match slot {
Some(slot) => {
assert!(!slot.is_dirty());
ImageSlot::PreviouslyUsed(slot)
}
None => ImageSlot::Unknown,
},
);
assert!(matches!(prev, ImageSlot::Unknown));
}
pub fn unused_warm_slots(&self) -> u32 {
self.stripes
.iter()
.map(|i| i.allocator.unused_warm_slots())
.sum()
}
pub fn unused_bytes_resident(&self) -> usize {
self.stripes
.iter()
.map(|i| i.allocator.unused_bytes_resident())
.sum()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct StripedAllocationIndex(u32);
impl StripedAllocationIndex {
fn from_unstriped_slot_index(
index: MemoryAllocationIndex,
num_stripes: usize,
) -> (usize, Self) {
let stripe_index = index.index() % num_stripes;
let num_stripes: u32 = num_stripes.try_into().unwrap();
let index_within_stripe = Self(index.0 / num_stripes);
(stripe_index, index_within_stripe)
}
fn as_unstriped_slot_index(self, stripe: usize, num_stripes: usize) -> MemoryAllocationIndex {
let num_stripes: u32 = num_stripes.try_into().unwrap();
let stripe: u32 = stripe.try_into().unwrap();
MemoryAllocationIndex(self.0 * num_stripes + stripe)
}
}
#[derive(Clone, Debug)]
struct SlabConstraints {
expected_slot_bytes: HostAlignedByteCount,
max_memory_bytes: HostAlignedByteCount,
num_slots: usize,
num_pkeys_available: usize,
guard_bytes: HostAlignedByteCount,
guard_before_slots: bool,
}
impl SlabConstraints {
fn new(
limits: &InstanceLimits,
tunables: &Tunables,
num_pkeys_available: usize,
) -> Result<Self> {
let expected_slot_bytes =
HostAlignedByteCount::new_rounded_up_u64(tunables.memory_reservation)
.context("memory reservation is too large")?;
let max_memory_bytes = HostAlignedByteCount::new_rounded_up(limits.max_memory_size)
.context("maximum size of memory is too large")?;
let guard_bytes = HostAlignedByteCount::new_rounded_up_u64(tunables.memory_guard_size)
.context("guard region is too large")?;
let num_slots = usize::try_from(limits.total_memories).context("too many memories")?;
let constraints = SlabConstraints {
max_memory_bytes,
num_slots,
expected_slot_bytes,
num_pkeys_available,
guard_bytes,
guard_before_slots: tunables.guard_before_linear_memory,
};
Ok(constraints)
}
}
#[derive(Debug)]
struct SlabLayout {
num_slots: usize,
slot_bytes: HostAlignedByteCount,
max_memory_bytes: HostAlignedByteCount,
pre_slab_guard_bytes: HostAlignedByteCount,
post_slab_guard_bytes: HostAlignedByteCount,
num_stripes: usize,
}
impl SlabLayout {
fn total_slab_bytes(&self) -> Result<HostAlignedByteCount> {
self.slot_bytes
.checked_mul(self.num_slots)
.and_then(|c| c.checked_add(self.pre_slab_guard_bytes))
.and_then(|c| c.checked_add(self.post_slab_guard_bytes))
.context("total size of memory reservation exceeds addressable memory")
}
fn bytes_to_next_stripe_slot(&self) -> HostAlignedByteCount {
self.slot_bytes
.checked_mul(self.num_stripes)
.expect("constructor checks that self.slot_bytes * self.num_stripes is in bounds")
}
}
fn calculate(constraints: &SlabConstraints) -> Result<SlabLayout> {
let SlabConstraints {
max_memory_bytes,
num_slots,
expected_slot_bytes,
num_pkeys_available,
guard_bytes,
guard_before_slots,
} = *constraints;
let pre_slab_guard_bytes = if guard_before_slots {
guard_bytes
} else {
HostAlignedByteCount::ZERO
};
let faulting_region_bytes = expected_slot_bytes
.max(max_memory_bytes)
.checked_add(guard_bytes)
.context("faulting region is too large")?;
let (num_stripes, slot_bytes) = if guard_bytes == 0 || max_memory_bytes == 0 || num_slots == 0 {
(1, faulting_region_bytes.byte_count())
} else if num_pkeys_available < 2 {
(1, faulting_region_bytes.byte_count())
} else {
let needed_num_stripes = faulting_region_bytes
.checked_div(max_memory_bytes)
.expect("if condition above implies max_memory_bytes is non-zero")
+ usize::from(
faulting_region_bytes
.checked_rem(max_memory_bytes)
.expect("if condition above implies max_memory_bytes is non-zero")
!= 0,
);
assert!(needed_num_stripes > 0);
let num_stripes = num_pkeys_available.min(needed_num_stripes).min(num_slots);
let needed_slot_bytes = faulting_region_bytes
.byte_count()
.checked_div(num_stripes)
.unwrap_or(faulting_region_bytes.byte_count())
.max(max_memory_bytes.byte_count());
assert!(needed_slot_bytes >= max_memory_bytes.byte_count());
(num_stripes, needed_slot_bytes)
};
let slot_bytes =
HostAlignedByteCount::new_rounded_up(slot_bytes).context("slot size is too large")?;
let post_slab_guard_bytes = faulting_region_bytes.saturating_sub(slot_bytes);
let layout = SlabLayout {
num_slots,
slot_bytes,
max_memory_bytes,
pre_slab_guard_bytes,
post_slab_guard_bytes,
num_stripes,
};
match layout.total_slab_bytes() {
Ok(_) => Ok(layout),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
const WASM_PAGE_SIZE: u32 = wasmtime_environ::Memory::DEFAULT_PAGE_SIZE;
#[cfg(target_pointer_width = "64")]
#[test]
fn test_memory_pool() -> Result<()> {
let pool = MemoryPool::new(
&PoolingInstanceAllocatorConfig {
limits: InstanceLimits {
total_memories: 5,
max_tables_per_module: 0,
max_memories_per_module: 3,
table_elements: 0,
max_memory_size: WASM_PAGE_SIZE as usize,
..Default::default()
},
..Default::default()
},
&Tunables {
memory_reservation: WASM_PAGE_SIZE as u64,
memory_guard_size: 0,
..Tunables::default_host()
},
)?;
assert_eq!(pool.layout.slot_bytes, WASM_PAGE_SIZE as usize);
assert_eq!(pool.layout.num_slots, 5);
assert_eq!(pool.layout.max_memory_bytes, WASM_PAGE_SIZE as usize);
let base = pool.mapping.as_ptr() as usize;
for i in 0..5 {
let index = MemoryAllocationIndex(i);
let ptr = pool.get_base(index).as_mut_ptr();
assert_eq!(
ptr as usize - base,
i as usize * pool.layout.slot_bytes.byte_count()
);
}
Ok(())
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_pooling_allocator_striping() {
if !mpk::is_supported() {
println!("skipping `test_pooling_allocator_striping` test; mpk is not supported");
return;
}
let config = PoolingInstanceAllocatorConfig {
memory_protection_keys: Enabled::Yes,
..PoolingInstanceAllocatorConfig::default()
};
let pool = MemoryPool::new(&config, &Tunables::default_host()).unwrap();
assert!(pool.stripes.len() >= 2);
let max_memory_slots = config.limits.total_memories;
dbg!(pool.stripes[0].allocator.num_empty_slots());
dbg!(pool.stripes[1].allocator.num_empty_slots());
let available_memory_slots: usize = pool
.stripes
.iter()
.map(|s| s.allocator.num_empty_slots())
.sum();
assert_eq!(
max_memory_slots,
u32::try_from(available_memory_slots).unwrap()
);
}
#[test]
fn check_known_layout_calculations() {
for num_pkeys_available in 0..16 {
for num_memory_slots in [0, 1, 10, 64] {
for expected_slot_bytes in [0, 1 << 30 , 4 << 30 ] {
let expected_slot_bytes =
HostAlignedByteCount::new(expected_slot_bytes).unwrap();
for max_memory_bytes in
[0, 1 * WASM_PAGE_SIZE as usize, 10 * WASM_PAGE_SIZE as usize]
{
let max_memory_bytes = HostAlignedByteCount::new(max_memory_bytes).unwrap();
for guard_bytes in [0, 2 << 30 ] {
let guard_bytes = HostAlignedByteCount::new(guard_bytes).unwrap();
for guard_before_slots in [true, false] {
let constraints = SlabConstraints {
max_memory_bytes,
num_slots: num_memory_slots,
expected_slot_bytes,
num_pkeys_available,
guard_bytes,
guard_before_slots,
};
match calculate(&constraints) {
Ok(layout) => {
assert_slab_layout_invariants(constraints, layout)
}
Err(e) => {
assert!(
cfg!(target_pointer_width = "32")
&& e.to_string()
.contains("exceeds addressable memory"),
"bad error: {e:?}"
);
}
}
}
}
}
}
}
}
}
proptest! {
#[test]
#[cfg_attr(miri, ignore)]
fn check_random_layout_calculations(c in constraints()) {
if let Ok(l) = calculate(&c) {
assert_slab_layout_invariants(c, l);
}
}
}
fn constraints() -> impl Strategy<Value = SlabConstraints> {
(
any::<HostAlignedByteCount>(),
any::<usize>(),
any::<HostAlignedByteCount>(),
any::<usize>(),
any::<HostAlignedByteCount>(),
any::<bool>(),
)
.prop_map(
|(
max_memory_bytes,
num_memory_slots,
expected_slot_bytes,
num_pkeys_available,
guard_bytes,
guard_before_slots,
)| {
SlabConstraints {
max_memory_bytes,
num_slots: num_memory_slots,
expected_slot_bytes,
num_pkeys_available,
guard_bytes,
guard_before_slots,
}
},
)
}
fn assert_slab_layout_invariants(c: SlabConstraints, s: SlabLayout) {
assert_eq!(
s.total_slab_bytes().unwrap(),
s.pre_slab_guard_bytes
.checked_add(s.slot_bytes.checked_mul(c.num_slots).unwrap())
.and_then(|c| c.checked_add(s.post_slab_guard_bytes))
.unwrap(),
"the slab size does not add up: {c:?} => {s:?}"
);
assert!(
s.slot_bytes >= s.max_memory_bytes,
"slot is not big enough: {c:?} => {s:?}"
);
assert!(s.num_stripes >= 1, "not enough stripes: {c:?} => {s:?}");
if c.num_pkeys_available == 0 || c.num_slots == 0 {
assert_eq!(
s.num_stripes, 1,
"expected at least one stripe: {c:?} => {s:?}"
);
} else {
assert!(
s.num_stripes <= c.num_pkeys_available,
"layout has more stripes than available pkeys: {c:?} => {s:?}"
);
assert!(
s.num_stripes <= c.num_slots,
"layout has more stripes than memory slots: {c:?} => {s:?}"
);
}
if c.num_pkeys_available > 1 && !c.max_memory_bytes.is_zero() {
assert!(
s.num_stripes <= (c.guard_bytes.checked_div(c.max_memory_bytes).unwrap() + 2),
"calculated more stripes than needed: {c:?} => {s:?}"
);
}
assert!(
s.bytes_to_next_stripe_slot()
>= c.expected_slot_bytes
.max(c.max_memory_bytes)
.checked_add(c.guard_bytes)
.unwrap(),
"faulting region not large enough: {c:?} => {s:?}"
);
assert!(
s.slot_bytes.checked_add(s.post_slab_guard_bytes).unwrap() >= c.expected_slot_bytes,
"last slot may allow OOB access: {c:?} => {s:?}"
);
}
}