#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
use std::time::Instant;
use std::{cell::Cell, error::Error, fmt, time::Duration};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use web_time::Instant;
pub const LARGE_RESULT_THRESHOLD: usize = 100_000;
#[derive(Debug, Clone)]
pub enum ResourceError {
Time { limit: Duration, elapsed: Duration },
Memory { limit: usize, used: usize },
Recursion { limit: usize, depth: usize },
}
impl fmt::Display for ResourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Time { limit, elapsed } => {
write!(f, "time limit exceeded: {elapsed:?} > {limit:?}")
}
Self::Memory { limit, used } => {
write!(f, "memory limit exceeded: {used} bytes > {limit} bytes")
}
Self::Recursion { .. } => {
write!(f, "maximum recursion depth exceeded")
}
}
}
}
impl Error for ResourceError {}
pub trait ResourceTracker: fmt::Debug {
fn on_free(&self, get_size: impl FnOnce() -> usize);
fn check_time(&self) -> Result<(), ResourceError>;
fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError>;
fn check_large_result(&self, estimated_bytes: usize) -> Result<(), ResourceError>;
fn on_grow(&self, get_additional: impl FnOnce() -> usize) -> Result<(), ResourceError>;
fn gc_interval(&self) -> Option<usize>;
fn on_execution_start(&self) {}
fn on_execution_stop(&self) {}
#[cfg(feature = "test-hooks")]
fn lower_recursion_limit(&self, _new_limit: usize) -> Result<(), Option<usize>> {
Err(None)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NoLimitTracker;
impl ResourceTracker for NoLimitTracker {
#[inline]
fn on_free(&self, _: impl FnOnce() -> usize) {}
#[inline]
fn check_time(&self) -> Result<(), ResourceError> {
Ok(())
}
#[inline]
fn on_grow(&self, _: impl FnOnce() -> usize) -> Result<(), ResourceError> {
Ok(())
}
#[inline]
fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError> {
const DEFAULT_RECURSION_LIMIT: usize = 1000;
if current_depth >= DEFAULT_RECURSION_LIMIT {
Err(ResourceError::Recursion {
limit: DEFAULT_RECURSION_LIMIT,
depth: current_depth + 1,
})
} else {
Ok(())
}
}
#[inline]
fn check_large_result(&self, _estimated_bytes: usize) -> Result<(), ResourceError> {
Ok(())
}
#[inline]
fn gc_interval(&self) -> Option<usize> {
None
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ResourceLimits {
pub max_duration: Option<Duration>,
pub max_memory: Option<usize>,
pub gc_interval: Option<usize>,
pub max_recursion_depth: Option<usize>,
}
pub const DEFAULT_MAX_RECURSION_DEPTH: usize = 1000;
impl ResourceLimits {
#[must_use]
pub fn new() -> Self {
Self {
max_recursion_depth: Some(1000),
..Default::default()
}
}
#[must_use]
pub fn max_duration(mut self, limit: Duration) -> Self {
self.max_duration = Some(limit);
self
}
#[must_use]
pub fn max_memory(mut self, limit: usize) -> Self {
self.max_memory = Some(limit);
self
}
#[must_use]
pub fn gc_interval(mut self, interval: usize) -> Self {
self.gc_interval = Some(interval);
self
}
#[must_use]
pub fn max_recursion_depth(mut self, limit: Option<usize>) -> Self {
self.max_recursion_depth = limit;
self
}
}
const TIME_CHECK_INTERVAL: u16 = 10;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct LimitedTracker {
limits: ResourceLimits,
#[serde(default)]
total_execution_time: Cell<Duration>,
#[serde(skip)]
running_since: Cell<Option<Instant>>,
current_memory: Cell<usize>,
check_counter: Cell<u16>,
#[serde(default)]
recursion_limit_override: Cell<Option<usize>>,
}
impl LimitedTracker {
#[must_use]
pub fn new(limits: ResourceLimits) -> Self {
Self {
limits,
total_execution_time: Cell::new(Duration::ZERO),
running_since: Cell::new(None),
current_memory: Cell::new(0),
check_counter: Cell::new(0),
recursion_limit_override: Cell::new(None),
}
}
fn active_recursion_limit(&self) -> Option<usize> {
self.recursion_limit_override.get().or(self.limits.max_recursion_depth)
}
#[must_use]
pub fn current_memory(&self) -> usize {
self.current_memory.get()
}
#[must_use]
pub fn elapsed(&self) -> Duration {
let running = self.running_since.get().map_or(Duration::ZERO, |t| t.elapsed());
self.total_execution_time.get() + running
}
#[must_use]
pub fn max_duration(&self) -> Option<Duration> {
self.limits.max_duration
}
pub fn set_max_duration(&mut self, duration: Duration) {
self.limits.max_duration = Some(duration);
self.total_execution_time.set(Duration::ZERO);
}
}
impl ResourceTracker for LimitedTracker {
fn on_free(&self, get_size: impl FnOnce() -> usize) {
if self.limits.max_memory.is_some() {
let current = self.current_memory.get();
self.current_memory.set(current.saturating_sub(get_size()));
}
}
fn on_grow(&self, get_additional: impl FnOnce() -> usize) -> Result<(), ResourceError> {
if let Some(max) = self.limits.max_memory {
let new_memory = self.current_memory.get().saturating_add(get_additional());
if new_memory > max {
return Err(ResourceError::Memory {
limit: max,
used: new_memory,
});
}
self.current_memory.set(new_memory);
}
Ok(())
}
fn check_time(&self) -> Result<(), ResourceError> {
if let Some(max) = self.limits.max_duration {
self.check_counter.update(|c| c.wrapping_add(1));
if self.check_counter.get().is_multiple_of(TIME_CHECK_INTERVAL) {
let elapsed = self.elapsed();
if elapsed > max {
self.check_counter.set(TIME_CHECK_INTERVAL.wrapping_sub(1));
return Err(ResourceError::Time { limit: max, elapsed });
}
}
}
Ok(())
}
fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError> {
if let Some(max) = self.active_recursion_limit() {
if current_depth >= max {
return Err(ResourceError::Recursion {
limit: max,
depth: current_depth + 1,
});
}
}
Ok(())
}
fn check_large_result(&self, estimated_bytes: usize) -> Result<(), ResourceError> {
if let Some(max) = self.limits.max_memory {
let new_memory = self.current_memory.get().saturating_add(estimated_bytes);
if new_memory > max {
return Err(ResourceError::Memory {
limit: max,
used: new_memory,
});
}
}
Ok(())
}
fn gc_interval(&self) -> Option<usize> {
self.limits.gc_interval
}
fn on_execution_start(&self) {
debug_assert!(
self.running_since.get().is_none(),
"nested on_execution_start: VM-internal re-entry must use the raw run loop, not run_external"
);
self.running_since.set(Some(Instant::now()));
}
fn on_execution_stop(&self) {
if let Some(started) = self.running_since.take() {
self.total_execution_time
.set(self.total_execution_time.get() + started.elapsed());
}
}
#[cfg(feature = "test-hooks")]
fn lower_recursion_limit(&self, new_limit: usize) -> Result<(), Option<usize>> {
if let Some(current) = self.active_recursion_limit()
&& new_limit > current
{
return Err(Some(current));
}
self.recursion_limit_override.set(Some(new_limit));
Ok(())
}
}