use std::{
cell::Cell,
error::Error,
fmt,
time::{Duration, Instant},
};
use crate::{
ExcType, MontyException,
exception_private::{ExceptionRaise, RawStackFrame, RunError, SimpleException},
};
pub const LARGE_RESULT_THRESHOLD: usize = 100_000;
pub fn check_repeat_size(item_len: usize, count: usize, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {
check_estimated_size(item_len.saturating_mul(count), tracker)
}
pub fn check_pow_size(base_bits: u64, exponent: u64, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {
if base_bits <= 1 {
return Ok(());
}
let result_bytes = estimate_bits_to_bytes(base_bits.saturating_mul(exponent));
check_estimated_size(result_bytes.saturating_mul(4), tracker)
}
pub fn check_mult_size(a_bits: u64, b_bits: u64, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {
check_estimated_size(estimate_bits_to_bytes(a_bits.saturating_add(b_bits)), tracker)
}
pub fn check_lshift_size(
value_bits: u64,
shift_amount: u64,
tracker: &impl ResourceTracker,
) -> Result<(), ResourceError> {
if value_bits == 0 {
return Ok(());
}
check_estimated_size(estimate_bits_to_bytes(value_bits.saturating_add(shift_amount)), tracker)
}
pub fn check_div_size(dividend_bits: u64, tracker: &impl ResourceTracker) -> Result<(), ResourceError> {
check_estimated_size(estimate_bits_to_bytes(dividend_bits), tracker)
}
pub fn check_replace_size(
input_len: usize,
old_len: usize,
new_len: usize,
count: i64,
tracker: &impl ResourceTracker,
) -> Result<(), ResourceError> {
let max_replacements = input_len
.checked_div(old_len)
.unwrap_or_else(|| input_len.saturating_add(1));
let replacements = if count < 0 {
max_replacements
} else {
max_replacements.min(usize::try_from(count).unwrap_or(usize::MAX))
};
let removed = replacements.saturating_mul(old_len);
let added = replacements.saturating_mul(new_len);
let estimated = input_len.saturating_sub(removed).saturating_add(added);
check_estimated_size(estimated, tracker)
}
pub(crate) fn check_estimated_size(
estimated_bytes: usize,
tracker: &impl ResourceTracker,
) -> Result<(), ResourceError> {
if estimated_bytes > LARGE_RESULT_THRESHOLD {
tracker.check_large_result(estimated_bytes)?;
}
Ok(())
}
fn estimate_bits_to_bytes(bits: u64) -> usize {
usize::try_from(bits.saturating_add(7) / 8).unwrap_or(usize::MAX)
}
#[derive(Debug, Clone)]
pub enum ResourceError {
Allocation { limit: usize, count: usize },
Time { limit: Duration, elapsed: Duration },
Memory { limit: usize, used: usize },
Recursion { limit: usize, depth: usize },
Exception(MontyException),
}
impl fmt::Display for ResourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Allocation { limit, count } => {
write!(f, "allocation limit exceeded: {count} > {limit}")
}
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")
}
Self::Exception(exc) => {
write!(f, "{exc}")
}
}
}
}
impl Error for ResourceError {}
impl ResourceError {
#[must_use]
pub(crate) fn into_exception(self, frame: Option<RawStackFrame>) -> ExceptionRaise {
let (exc_type, msg) = match self {
Self::Allocation { limit, count } => (
ExcType::MemoryError,
Some(format!("allocation limit exceeded: {count} > {limit}")),
),
Self::Memory { limit, used } => (
ExcType::MemoryError,
Some(format!("memory limit exceeded: {used} bytes > {limit} bytes")),
),
Self::Time { limit, elapsed } => (
ExcType::TimeoutError,
Some(format!("time limit exceeded: {elapsed:?} > {limit:?}")),
),
Self::Recursion { .. } => (
ExcType::RecursionError,
Some("maximum recursion depth exceeded".to_string()),
),
Self::Exception(exc) => (exc.exc_type(), exc.into_message()),
};
let exc = SimpleException::new(exc_type, msg);
match frame {
Some(f) => exc.with_frame(f),
None => exc.into(),
}
}
}
impl From<ResourceError> for RunError {
fn from(err: ResourceError) -> Self {
if matches!(err, ResourceError::Recursion { .. }) {
Self::Exc(err.into_exception(None))
} else {
Self::UncatchableExc(err.into_exception(None))
}
}
}
pub trait ResourceTracker: fmt::Debug {
fn on_allocate(&self, get_size: impl FnOnce() -> usize) -> Result<(), ResourceError>;
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, additional_bytes: 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_allocate(&self, _: impl FnOnce() -> usize) -> Result<(), ResourceError> {
Ok(())
}
#[inline]
fn on_free(&self, _: impl FnOnce() -> usize) {}
#[inline]
fn check_time(&self) -> Result<(), ResourceError> {
Ok(())
}
#[inline]
fn on_grow(&self, _: 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_allocations: Option<usize>,
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_allocations(mut self, limit: usize) -> Self {
self.max_allocations = Some(limit);
self
}
#[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>>,
allocation_count: Cell<usize>,
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),
allocation_count: Cell::new(0),
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 allocation_count(&self) -> usize {
self.allocation_count.get()
}
#[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_allocate(&self, get_size: impl FnOnce() -> usize) -> Result<(), ResourceError> {
let count = self.allocation_count.get();
if let Some(max) = self.limits.max_allocations
&& count >= max
{
return Err(ResourceError::Allocation {
limit: max,
count: count + 1,
});
}
let size = get_size();
let current_mem = self.current_memory.get();
if let Some(max) = self.limits.max_memory {
let new_memory = current_mem + size;
if new_memory > max {
return Err(ResourceError::Memory {
limit: max,
used: new_memory,
});
}
}
self.allocation_count.set(count + 1);
self.current_memory.set(current_mem + size);
Ok(())
}
fn on_free(&self, get_size: impl FnOnce() -> usize) {
let current = self.current_memory.get();
self.current_memory.set(current.saturating_sub(get_size()));
}
fn on_grow(&self, additional_bytes: usize) -> Result<(), ResourceError> {
let current_mem = self.current_memory.get();
let new_memory = current_mem.saturating_add(additional_bytes);
if let Some(max) = self.limits.max_memory
&& 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(())
}
}