use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
time::Duration,
};
use wasmtime::{Engine, ResourceLimiter, StoreLimits, StoreLimitsBuilder};
pub(crate) const EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(100);
pub const DEFAULT_MEMORY_BYTES: usize = 512 * 1024 * 1024;
pub const DEFAULT_TABLE_ELEMENTS: usize = 100_000;
pub const DEFAULT_INSTANCES: usize = 1_000;
pub const DEFAULT_DEADLINE_SECONDS: u64 = 600;
pub const HOOK_DEFAULT_DEADLINE_SECONDS: u64 = 30;
const _: () = assert!(
HOOK_DEFAULT_DEADLINE_SECONDS < DEFAULT_DEADLINE_SECONDS,
"a hook call must not be able to stall a session for the capsule-wide budget"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExecutionLimits {
pub memory_bytes: usize,
pub table_elements: usize,
pub instances: usize,
pub deadline_seconds: u64,
}
impl Default for ExecutionLimits {
fn default() -> Self {
Self {
memory_bytes: DEFAULT_MEMORY_BYTES,
table_elements: DEFAULT_TABLE_ELEMENTS,
instances: DEFAULT_INSTANCES,
deadline_seconds: DEFAULT_DEADLINE_SECONDS,
}
}
}
impl ExecutionLimits {
#[must_use]
pub fn resolve(declared: Option<&murmur_artifact::ResourceLimits>) -> Self {
let defaults = Self::default();
let Some(declared) = declared else {
return defaults;
};
Self {
memory_bytes: declared.memory_bytes.unwrap_or(defaults.memory_bytes),
table_elements: declared.table_elements.unwrap_or(defaults.table_elements),
instances: declared.instances.unwrap_or(defaults.instances),
deadline_seconds: declared
.deadline_seconds
.unwrap_or(defaults.deadline_seconds),
}
}
#[must_use]
pub fn for_hook_calls(self, declared_deadline_seconds: Option<u64>) -> Self {
Self {
deadline_seconds: declared_deadline_seconds.unwrap_or(HOOK_DEFAULT_DEADLINE_SECONDS),
..self
}
}
pub(crate) fn deadline_ticks(&self) -> u64 {
let tick_ms = EPOCH_TICK_INTERVAL.as_millis() as u64;
(self.deadline_seconds.saturating_mul(1_000) / tick_ms).max(1)
}
pub(crate) fn limiter(&self) -> ExecutionLimiter {
ExecutionLimiter::new(*self)
}
}
pub(crate) struct ExecutionLimiter {
inner: StoreLimits,
limits: ExecutionLimits,
denial: Option<String>,
}
impl ExecutionLimiter {
fn new(limits: ExecutionLimits) -> Self {
let inner = StoreLimitsBuilder::new()
.memory_size(limits.memory_bytes)
.table_elements(limits.table_elements)
.instances(limits.instances)
.trap_on_grow_failure(false)
.build();
Self {
inner,
limits,
denial: None,
}
}
pub(crate) fn limits(&self) -> ExecutionLimits {
self.limits
}
pub(crate) fn denial(&self) -> Option<&str> {
self.denial.as_deref()
}
}
impl ResourceLimiter for ExecutionLimiter {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> wasmtime::Result<bool> {
if self.inner.memory_growing(current, desired, maximum)? {
return Ok(true);
}
let message = format!(
"linear memory growth to {desired} bytes exceeds \
capabilities.limits.memory_bytes ({})",
self.limits.memory_bytes
);
self.denial = Some(message.clone());
Err(wasmtime::Error::msg(message))
}
fn table_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> wasmtime::Result<bool> {
if self.inner.table_growing(current, desired, maximum)? {
return Ok(true);
}
let message = format!(
"table growth to {desired} elements exceeds \
capabilities.limits.table_elements ({})",
self.limits.table_elements
);
self.denial = Some(message.clone());
Err(wasmtime::Error::msg(message))
}
fn instances(&self) -> usize {
self.inner.instances()
}
fn tables(&self) -> usize {
self.inner.tables()
}
fn memories(&self) -> usize {
self.inner.memories()
}
}
pub(crate) struct EpochTicker {
stop: Arc<AtomicBool>,
}
impl EpochTicker {
pub(crate) fn spawn(engine: &Engine) -> Self {
let weak = engine.weak();
let stop = Arc::new(AtomicBool::new(false));
let thread_stop = Arc::clone(&stop);
thread::spawn(move || {
while !thread_stop.load(Ordering::Relaxed) {
thread::sleep(EPOCH_TICK_INTERVAL);
match weak.upgrade() {
Some(engine) => engine.increment_epoch(),
None => break,
}
}
});
Self { stop }
}
}
impl Drop for EpochTicker {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum GuestFailure {
DeadlineExceeded { seconds: u64 },
ResourceLimit { message: String },
Other,
}
pub(crate) fn classify_guest_failure(
error: &wasmtime::Error,
limiter: &ExecutionLimiter,
) -> GuestFailure {
if let Some(message) = limiter.denial() {
return GuestFailure::ResourceLimit {
message: message.to_string(),
};
}
if matches!(
error.downcast_ref::<wasmtime::Trap>(),
Some(wasmtime::Trap::Interrupt)
) {
return GuestFailure::DeadlineExceeded {
seconds: limiter.limits().deadline_seconds,
};
}
GuestFailure::Other
}
impl GuestFailure {
pub(crate) fn message(&self, subject: &str, error: &wasmtime::Error) -> String {
match self {
Self::DeadlineExceeded { seconds } => format!(
"{subject} exceeded its {seconds}s execution deadline \
(capabilities.limits.deadline_seconds) and was interrupted"
),
Self::ResourceLimit { message } => {
format!("{subject} exceeded its configured resource limits: {message}")
}
Self::Other => format!("{subject} trapped: {error}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_uses_defaults_when_manifest_is_silent() {
assert_eq!(ExecutionLimits::resolve(None), ExecutionLimits::default());
}
#[test]
fn resolve_fills_each_omitted_field_independently() {
let declared = murmur_artifact::ResourceLimits {
memory_bytes: Some(1024),
table_elements: None,
instances: None,
deadline_seconds: Some(5),
};
let limits = ExecutionLimits::resolve(Some(&declared));
assert_eq!(limits.memory_bytes, 1024);
assert_eq!(limits.deadline_seconds, 5);
assert_eq!(limits.table_elements, DEFAULT_TABLE_ELEMENTS);
assert_eq!(limits.instances, DEFAULT_INSTANCES);
}
#[test]
fn hook_calls_get_the_lower_deadline_default_when_none_is_declared() {
let capsule = ExecutionLimits::resolve(None);
let hook = capsule.for_hook_calls(None);
assert_eq!(capsule.deadline_seconds, DEFAULT_DEADLINE_SECONDS);
assert_eq!(hook.deadline_seconds, HOOK_DEFAULT_DEADLINE_SECONDS);
assert_eq!(hook.memory_bytes, capsule.memory_bytes);
assert_eq!(hook.table_elements, capsule.table_elements);
assert_eq!(hook.instances, capsule.instances);
}
#[test]
fn an_explicit_deadline_is_honored_for_hook_calls() {
let declared = murmur_artifact::ResourceLimits {
memory_bytes: None,
table_elements: None,
instances: None,
deadline_seconds: Some(DEFAULT_DEADLINE_SECONDS),
};
let capsule = ExecutionLimits::resolve(Some(&declared));
assert_eq!(
capsule
.for_hook_calls(declared.deadline_seconds)
.deadline_seconds,
DEFAULT_DEADLINE_SECONDS
);
assert_eq!(capsule.for_hook_calls(Some(5)).deadline_seconds, 5);
}
#[test]
fn deadline_ticks_converts_seconds_and_never_yields_zero() {
let limits = ExecutionLimits {
deadline_seconds: 1,
..ExecutionLimits::default()
};
assert_eq!(limits.deadline_ticks(), 10);
assert_eq!(ExecutionLimits::default().deadline_ticks(), 6_000);
}
#[test]
fn classify_reports_resource_limit_after_a_denial() {
let limits = ExecutionLimits {
memory_bytes: 1024,
..ExecutionLimits::default()
};
let mut limiter = limits.limiter();
let error = limiter
.memory_growing(0, 4096, None)
.expect_err("growth past memory_bytes must be denied");
match classify_guest_failure(&error, &limiter) {
GuestFailure::ResourceLimit { message } => {
assert!(message.contains("capabilities.limits.memory_bytes"));
}
other => panic!("expected ResourceLimit, got {other:?}"),
}
}
#[test]
fn classify_reports_deadline_for_an_interrupt_trap() {
let limits = ExecutionLimits {
deadline_seconds: 7,
..ExecutionLimits::default()
};
let limiter = limits.limiter();
let error = wasmtime::Error::from(wasmtime::Trap::Interrupt);
assert_eq!(
classify_guest_failure(&error, &limiter),
GuestFailure::DeadlineExceeded { seconds: 7 }
);
}
#[test]
fn classify_reports_other_for_an_unrelated_trap() {
let limiter = ExecutionLimits::default().limiter();
let error = wasmtime::Error::from(wasmtime::Trap::UnreachableCodeReached);
let failure = classify_guest_failure(&error, &limiter);
assert_eq!(failure, GuestFailure::Other);
assert!(failure
.message("tool 'echo'", &error)
.starts_with("tool 'echo' trapped: "));
}
#[test]
fn limiter_allows_growth_within_the_cap() {
let limits = ExecutionLimits {
memory_bytes: 4096,
..ExecutionLimits::default()
};
let mut limiter = limits.limiter();
assert!(limiter.memory_growing(0, 4096, None).unwrap());
assert!(limiter.denial().is_none());
}
}