use super::{
index_allocator::{MemoryInModule, ModuleAffinityIndexAllocator, SlotId},
MemoryAllocationIndex,
};
use crate::mpk::{self, ProtectionKey, ProtectionMask};
use crate::{
CompiledModuleId, InstanceAllocationRequest, InstanceLimits, Memory, MemoryImageSlot, Mmap,
MpkEnabled, PoolingInstanceAllocatorConfig,
};
use anyhow::{anyhow, bail, Context, Result};
use std::ffi::c_void;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use wasmtime_environ::{
DefinedMemoryIndex, MemoryPlan, MemoryStyle, Module, Tunables, WASM_PAGE_SIZE,
};
#[derive(Debug)]
struct Stripe {
allocator: ModuleAffinityIndexAllocator,
pkey: Option<ProtectionKey>,
}
#[derive(Debug)]
pub struct MemoryPool {
mapping: Mmap,
stripes: Vec<Stripe>,
image_slots: Vec<Mutex<Option<MemoryImageSlot>>>,
layout: SlabLayout,
memories_per_instance: usize,
keep_resident: usize,
next_available_pkey: AtomicUsize,
}
impl MemoryPool {
pub fn new(config: &PoolingInstanceAllocatorConfig, tunables: &Tunables) -> Result<Self> {
if config.limits.memory_pages > 0x10000 {
bail!(
"module memory page limit of {} exceeds the maximum of 65536",
config.limits.memory_pages
);
}
let pkeys = match config.memory_protection_keys {
MpkEnabled::Auto => {
if mpk::is_supported() {
mpk::keys(config.max_memory_protection_keys)
} else {
&[]
}
}
MpkEnabled::Enable => {
if mpk::is_supported() {
mpk::keys(config.max_memory_protection_keys)
} else {
bail!("mpk is disabled on this system")
}
}
MpkEnabled::Disable => &[],
};
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(0, 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..cursor + layout.slot_bytes) };
pkey.protect(region)?;
cursor += layout.slot_bytes;
}
debug_assert_eq!(
cursor + layout.post_slab_guard_bytes,
layout.total_slab_bytes()?
);
}
let image_slots: Vec<_> = std::iter::repeat_with(|| Mutex::new(None))
.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)
.into_iter()
.map(create_stripe)
.collect();
let pool = Self {
stripes,
mapping,
image_slots,
layout,
memories_per_instance: usize::try_from(config.limits.max_memories_per_module).unwrap(),
keep_resident: 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.clone()
}
pub fn validate(&self, module: &Module) -> Result<()> {
let memories = module.memory_plans.len() - module.num_imported_memories;
if memories > usize::try_from(self.memories_per_instance).unwrap() {
bail!(
"defined memories count of {} exceeds the per-instance limit of {}",
memories,
self.memories_per_instance,
);
}
let max_memory_pages = self.layout.max_memory_bytes / WASM_PAGE_SIZE as usize;
for (i, plan) in module
.memory_plans
.iter()
.skip(module.num_imported_memories)
{
match plan.style {
MemoryStyle::Static { bound } => {
if self.layout.pages_to_next_stripe_slot() < bound {
bail!(
"memory size allocated per-memory is too small to \
satisfy static bound of {bound:#x} pages"
);
}
}
MemoryStyle::Dynamic { .. } => {}
}
if plan.memory.minimum > u64::try_from(max_memory_pages).unwrap() {
bail!(
"memory index {} has a minimum page size of {} which exceeds the limit of {}",
i.as_u32(),
plan.memory.minimum,
max_memory_pages,
);
}
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.stripes.iter().all(|s| s.allocator.is_empty())
}
pub fn allocate(
&self,
request: &mut InstanceAllocationRequest,
memory_plan: &MemoryPlan,
memory_index: DefinedMemoryIndex,
) -> Result<(MemoryAllocationIndex, Memory)> {
let stripe_index = if let Some(pkey) = &request.pkey {
pkey.as_stripe()
} else {
debug_assert!(self.stripes.len() < 2);
0
};
let striped_allocation_index = self.stripes[stripe_index]
.allocator
.alloc(
request
.runtime_info
.unique_id()
.map(|id| MemoryInModule(id, memory_index)),
)
.map(|slot| StripedAllocationIndex(u32::try_from(slot.index()).unwrap()))
.ok_or_else(|| {
anyhow!(
"maximum concurrent memory limit of {} reached for stripe {}",
self.stripes[stripe_index].allocator.len(),
stripe_index
)
})?;
let allocation_index =
striped_allocation_index.as_unstriped_slot_index(stripe_index, self.stripes.len());
match (|| {
match memory_plan.style {
MemoryStyle::Static { bound } => {
assert!(bound <= self.layout.pages_to_next_stripe_slot());
}
MemoryStyle::Dynamic { .. } => {}
}
let base_ptr = 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 = request.runtime_info.memory_image(memory_index)?;
let initial_size = memory_plan.memory.minimum * WASM_PAGE_SIZE as u64;
slot.instantiate(initial_size as usize, image, memory_plan)?;
Memory::new_static(
memory_plan,
base_ptr,
base_capacity,
slot,
self.layout.bytes_to_next_stripe_slot(),
unsafe { &mut *request.store.get().unwrap() },
)
})() {
Ok(memory) => Ok((allocation_index, memory)),
Err(e) => {
self.stripes[stripe_index]
.allocator
.free(SlotId(striped_allocation_index.0));
Err(e)
}
}
}
pub unsafe fn deallocate(&self, allocation_index: MemoryAllocationIndex, memory: Memory) {
let mut image = memory.unwrap_static_image();
if image.clear_and_remain_ready(self.keep_resident).is_ok() {
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));
}
pub fn purge_module(&self, module: CompiledModuleId) {
for stripe in &self.stripes {
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 = MemoryAllocationIndex(id.0);
let mut slot = self.take_memory_image_slot(index);
if slot.remove_image().is_ok() {
self.return_memory_image_slot(index, slot);
}
stripe.allocator.free(id);
}
}
}
}
fn get_base(&self, allocation_index: MemoryAllocationIndex) -> *mut u8 {
assert!(allocation_index.index() < self.layout.num_slots);
let offset =
self.layout.pre_slab_guard_bytes + allocation_index.index() * self.layout.slot_bytes;
unsafe { self.mapping.as_ptr().offset(offset as isize).cast_mut() }
}
fn take_memory_image_slot(&self, allocation_index: MemoryAllocationIndex) -> MemoryImageSlot {
let maybe_slot = self.image_slots[allocation_index.index()]
.lock()
.unwrap()
.take();
maybe_slot.unwrap_or_else(|| {
MemoryImageSlot::create(
self.get_base(allocation_index) as *mut c_void,
0,
self.layout.max_memory_bytes,
)
})
}
fn return_memory_image_slot(
&self,
allocation_index: MemoryAllocationIndex,
slot: MemoryImageSlot,
) {
assert!(!slot.is_dirty());
*self.image_slots[allocation_index.index()].lock().unwrap() = Some(slot);
}
}
impl Drop for MemoryPool {
fn drop(&mut self) {
for mut slot in std::mem::take(&mut self.image_slots) {
if let Some(slot) = slot.get_mut().unwrap() {
slot.no_clear_on_drop();
}
}
}
}
#[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: usize,
max_memory_bytes: usize,
num_slots: usize,
num_pkeys_available: usize,
guard_bytes: usize,
guard_before_slots: bool,
}
impl SlabConstraints {
fn new(
limits: &InstanceLimits,
tunables: &Tunables,
num_pkeys_available: usize,
) -> Result<Self> {
let max_memory_bytes = limits.memory_pages * u64::from(WASM_PAGE_SIZE);
let expected_slot_bytes = tunables.static_memory_bound * u64::from(WASM_PAGE_SIZE);
let constraints = SlabConstraints {
max_memory_bytes: max_memory_bytes
.try_into()
.context("max memory is too large")?,
num_slots: limits
.total_memories
.try_into()
.context("too many memories")?,
expected_slot_bytes: expected_slot_bytes
.try_into()
.context("static memory bound is too large")?,
num_pkeys_available,
guard_bytes: tunables
.static_memory_offset_guard_size
.try_into()
.context("guard region is too large")?,
guard_before_slots: tunables.guard_before_linear_memory,
};
Ok(constraints)
}
}
#[derive(Debug)]
struct SlabLayout {
num_slots: usize,
slot_bytes: usize,
max_memory_bytes: usize,
pre_slab_guard_bytes: usize,
post_slab_guard_bytes: usize,
num_stripes: usize,
}
impl SlabLayout {
fn total_slab_bytes(&self) -> Result<usize> {
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))
.ok_or_else(|| anyhow!("total size of memory reservation exceeds addressable memory"))
}
fn bytes_to_next_stripe_slot(&self) -> usize {
self.slot_bytes * self.num_stripes
}
fn pages_to_next_stripe_slot(&self) -> u64 {
let bytes = self.bytes_to_next_stripe_slot();
let pages = bytes / WASM_PAGE_SIZE as usize;
u64::try_from(pages).unwrap()
}
}
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 { 0 };
let faulting_region_bytes = expected_slot_bytes
.max(max_memory_bytes)
.saturating_add(guard_bytes);
let (num_stripes, slot_bytes) = if guard_bytes == 0 || max_memory_bytes == 0 || num_slots == 0 {
(1, faulting_region_bytes)
} else if num_pkeys_available < 2 {
(1, faulting_region_bytes)
} else {
let needed_num_stripes = faulting_region_bytes / max_memory_bytes
+ usize::from(faulting_region_bytes % max_memory_bytes != 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
.checked_div(num_stripes)
.unwrap_or(faulting_region_bytes)
.max(max_memory_bytes);
assert!(needed_slot_bytes >= max_memory_bytes);
(num_stripes, needed_slot_bytes)
};
let page_alignment = crate::page_size() - 1;
let slot_bytes = slot_bytes
.checked_add(page_alignment)
.and_then(|slot_bytes| Some(slot_bytes & !page_alignment))
.ok_or_else(|| anyhow!("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 crate::PoolingInstanceAllocator;
use proptest::prelude::*;
#[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,
memory_pages: 1,
..Default::default()
},
..Default::default()
},
&Tunables {
static_memory_bound: 1,
static_memory_offset_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);
assert_eq!(ptr as usize - base, i as usize * pool.layout.slot_bytes);
}
Ok(())
}
#[test]
fn test_pooling_allocator_with_reservation_size_exceeded() {
let config = PoolingInstanceAllocatorConfig {
limits: InstanceLimits {
total_memories: 1,
memory_pages: 2,
..Default::default()
},
..PoolingInstanceAllocatorConfig::default()
};
let pool = PoolingInstanceAllocator::new(
&config,
&Tunables {
static_memory_bound: 1,
static_memory_offset_guard_size: 0,
..Tunables::default_host()
},
)
.unwrap();
assert_eq!(pool.memories.layout.max_memory_bytes, 2 * 65536);
}
#[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: MpkEnabled::Enable,
..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, available_memory_slots.try_into().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 ] {
for max_memory_bytes in
[0, 1 * WASM_PAGE_SIZE as usize, 10 * WASM_PAGE_SIZE as usize]
{
for guard_bytes in [0, 2 << 30 ] {
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,
};
let layout = calculate(&constraints);
assert_slab_layout_invariants(constraints, layout.unwrap());
}
}
}
}
}
}
}
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::<usize>(),
any::<usize>(),
any::<usize>(),
any::<usize>(),
any::<usize>(),
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 + s.slot_bytes * c.num_slots + s.post_slab_guard_bytes,
"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!(
is_aligned(s.slot_bytes),
"slot is not page-aligned: {c:?} => {s:?}",
);
assert!(
is_aligned(s.max_memory_bytes),
"slot guard region is not page-aligned: {c:?} => {s:?}",
);
assert!(
is_aligned(s.pre_slab_guard_bytes),
"pre-slab guard region is not page-aligned: {c:?} => {s:?}"
);
assert!(
is_aligned(s.post_slab_guard_bytes),
"post-slab guard region is not page-aligned: {c:?} => {s:?}"
);
assert!(
is_aligned(s.total_slab_bytes().unwrap()),
"slab is not page-aligned: {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 > 0 {
assert!(
s.num_stripes <= (c.guard_bytes / c.max_memory_bytes) + 2,
"calculated more stripes than needed: {c:?} => {s:?}"
);
}
assert!(
s.bytes_to_next_stripe_slot()
>= c.expected_slot_bytes.max(c.max_memory_bytes) + c.guard_bytes,
"faulting region not large enough: {c:?} => {s:?}"
);
assert!(
s.slot_bytes + s.post_slab_guard_bytes >= c.expected_slot_bytes,
"last slot may allow OOB access: {c:?} => {s:?}"
);
}
fn is_aligned(bytes: usize) -> bool {
bytes % crate::page_size() == 0
}
}