use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::time_compat::Instant;
#[cfg(feature = "failpoints")]
use fail::fail_point;
#[derive(Debug, Clone)]
pub struct ExecutionLimits {
pub max_work_units: u64,
pub max_aggregate_input_bytes: u64,
pub max_live_intermediate_bytes: u64,
pub max_commands: usize,
pub max_loop_iterations: usize,
pub max_total_loop_iterations: usize,
pub max_function_depth: usize,
pub timeout: Duration,
pub parser_timeout: Duration,
pub max_input_bytes: usize,
pub max_ast_depth: usize,
pub max_parser_operations: usize,
pub max_stdout_bytes: usize,
pub max_stderr_bytes: usize,
pub max_subst_depth: usize,
pub max_subshell_depth: usize,
pub max_file_descriptors: usize,
pub max_history_entries: usize,
pub max_history_bytes: usize,
pub max_history_output_bytes: usize,
pub max_word_split_fields: usize,
pub max_word_split_bytes: usize,
pub capture_final_env: bool,
}
impl Default for ExecutionLimits {
fn default() -> Self {
Self {
max_work_units: 100_000_000,
max_aggregate_input_bytes: 100_000_000,
max_live_intermediate_bytes: 32_000_000,
max_commands: 10_000,
max_loop_iterations: 10_000,
max_total_loop_iterations: 1_000_000,
max_function_depth: 100,
timeout: Duration::from_secs(30),
parser_timeout: Duration::from_secs(5),
max_input_bytes: 10_000_000, max_ast_depth: 100,
max_parser_operations: 100_000,
max_stdout_bytes: 1_048_576, max_stderr_bytes: 1_048_576, max_subst_depth: 32,
max_subshell_depth: 32,
max_file_descriptors: 1024,
max_history_entries: 1_000,
max_history_bytes: 1_048_576, max_history_output_bytes: 1_048_576, max_word_split_fields: 100_000,
max_word_split_bytes: 10_000_000,
capture_final_env: false,
}
}
}
impl ExecutionLimits {
pub fn new() -> Self {
Self::default()
}
pub fn max_work_units(mut self, units: u64) -> Self {
self.max_work_units = units;
self
}
pub fn max_aggregate_input_bytes(mut self, bytes: u64) -> Self {
self.max_aggregate_input_bytes = bytes;
self
}
pub fn max_live_intermediate_bytes(mut self, bytes: u64) -> Self {
self.max_live_intermediate_bytes = bytes;
self
}
pub fn cli() -> Self {
Self {
max_commands: usize::MAX,
max_loop_iterations: usize::MAX,
max_total_loop_iterations: usize::MAX,
timeout: Duration::from_secs(u64::MAX / 2), max_stdout_bytes: 10_485_760, max_stderr_bytes: 10_485_760, max_history_output_bytes: 10_485_760, ..Self::default()
}
}
pub fn max_commands(mut self, count: usize) -> Self {
self.max_commands = count;
self
}
pub fn max_loop_iterations(mut self, count: usize) -> Self {
self.max_loop_iterations = count;
self
}
pub fn max_total_loop_iterations(mut self, count: usize) -> Self {
self.max_total_loop_iterations = count;
self
}
pub fn max_function_depth(mut self, depth: usize) -> Self {
self.max_function_depth = depth;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn parser_timeout(mut self, timeout: Duration) -> Self {
self.parser_timeout = timeout;
self
}
pub fn max_input_bytes(mut self, bytes: usize) -> Self {
self.max_input_bytes = bytes;
self
}
pub fn max_ast_depth(mut self, depth: usize) -> Self {
self.max_ast_depth = depth;
self
}
pub fn max_parser_operations(mut self, ops: usize) -> Self {
self.max_parser_operations = ops;
self
}
pub fn max_stdout_bytes(mut self, bytes: usize) -> Self {
self.max_stdout_bytes = bytes;
self
}
pub fn max_stderr_bytes(mut self, bytes: usize) -> Self {
self.max_stderr_bytes = bytes;
self
}
pub fn max_subst_depth(mut self, depth: usize) -> Self {
self.max_subst_depth = depth;
self
}
pub fn max_subshell_depth(mut self, depth: usize) -> Self {
self.max_subshell_depth = depth;
self
}
pub fn max_file_descriptors(mut self, count: usize) -> Self {
self.max_file_descriptors = count;
self
}
pub fn max_history_entries(mut self, count: usize) -> Self {
self.max_history_entries = count;
self
}
pub fn max_history_bytes(mut self, bytes: usize) -> Self {
self.max_history_bytes = bytes;
self
}
pub fn max_history_output_bytes(mut self, bytes: usize) -> Self {
self.max_history_output_bytes = bytes;
self
}
pub fn max_word_split_fields(mut self, count: usize) -> Self {
if count > 0 {
self.max_word_split_fields = count;
}
self
}
pub fn max_word_split_bytes(mut self, bytes: usize) -> Self {
if bytes > 0 {
self.max_word_split_bytes = bytes;
}
self
}
pub fn capture_final_env(mut self, capture: bool) -> Self {
self.capture_final_env = capture;
self
}
}
pub const DEFAULT_SESSION_MAX_COMMANDS: u64 = 100_000;
pub const DEFAULT_SESSION_MAX_EXEC_CALLS: u64 = 1_000;
#[derive(Debug, Clone)]
pub struct SessionLimits {
pub max_total_commands: u64,
pub max_exec_calls: u64,
}
impl Default for SessionLimits {
fn default() -> Self {
Self {
max_total_commands: DEFAULT_SESSION_MAX_COMMANDS,
max_exec_calls: DEFAULT_SESSION_MAX_EXEC_CALLS,
}
}
}
impl SessionLimits {
pub fn new() -> Self {
Self::default()
}
pub fn max_total_commands(mut self, count: u64) -> Self {
self.max_total_commands = count;
self
}
pub fn max_exec_calls(mut self, count: u64) -> Self {
self.max_exec_calls = count;
self
}
pub fn unlimited() -> Self {
Self {
max_total_commands: u64::MAX,
max_exec_calls: u64::MAX,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ExecutionCounters {
pub commands: usize,
pub function_depth: usize,
pub loop_iterations: Vec<usize>,
pub total_loop_iterations: usize,
pub subst_depth: usize,
pub subshell_depth: usize,
pub session_commands: u64,
pub session_exec_calls: u64,
}
impl ExecutionCounters {
pub fn new() -> Self {
Self::default()
}
pub fn reset_for_execution(&mut self) {
self.commands = 0;
self.loop_iterations.clear();
self.total_loop_iterations = 0;
self.function_depth = 0;
self.subst_depth = 0;
self.subshell_depth = 0;
}
pub fn tick_command(&mut self, limits: &ExecutionLimits) -> Result<(), LimitExceeded> {
#[cfg(feature = "failpoints")]
fail_point!("limits::tick_command", |action| {
match action.as_deref() {
Some("skip_increment") => {
return Ok(());
}
Some("force_overflow") => {
self.commands = usize::MAX;
return Err(LimitExceeded::MaxCommands(limits.max_commands));
}
Some("corrupt_high") => {
self.commands = limits.max_commands + 1;
}
_ => {}
}
Ok(())
});
self.commands = self.commands.saturating_add(1);
self.session_commands = self.session_commands.saturating_add(1);
if self.commands > limits.max_commands {
return Err(LimitExceeded::MaxCommands(limits.max_commands));
}
Ok(())
}
pub fn check_session_limits(
&self,
session_limits: &SessionLimits,
) -> Result<(), LimitExceeded> {
if self.session_exec_calls > session_limits.max_exec_calls {
return Err(LimitExceeded::SessionMaxExecCalls(
session_limits.max_exec_calls,
));
}
if self.session_commands > session_limits.max_total_commands {
return Err(LimitExceeded::SessionMaxCommands(
session_limits.max_total_commands,
));
}
Ok(())
}
pub fn tick_exec_call(&mut self) {
self.session_exec_calls = self.session_exec_calls.saturating_add(1);
}
pub fn tick_loop(&mut self, limits: &ExecutionLimits) -> Result<(), LimitExceeded> {
#[cfg(feature = "failpoints")]
fail_point!("limits::tick_loop", |action| {
match action.as_deref() {
Some("skip_check") => {
if let Some(current) = self.loop_iterations.last_mut() {
*current += 1;
}
return Ok(());
}
Some("reset_counter") => {
if let Some(current) = self.loop_iterations.last_mut() {
*current = 0;
}
return Ok(());
}
_ => {}
}
Ok(())
});
if self.loop_iterations.is_empty() {
self.loop_iterations.push(0);
}
let current = self
.loop_iterations
.last_mut()
.expect("loop stack initialized above");
*current += 1;
self.total_loop_iterations += 1;
if *current > limits.max_loop_iterations {
return Err(LimitExceeded::MaxLoopIterations(limits.max_loop_iterations));
}
if self.total_loop_iterations > limits.max_total_loop_iterations {
return Err(LimitExceeded::MaxTotalLoopIterations(
limits.max_total_loop_iterations,
));
}
Ok(())
}
pub fn enter_loop(&mut self) {
self.loop_iterations.push(0);
}
pub fn exit_loop(&mut self) {
self.loop_iterations.pop();
}
pub fn push_function(&mut self, limits: &ExecutionLimits) -> Result<(), LimitExceeded> {
#[cfg(feature = "failpoints")]
fail_point!("limits::push_function", |action| {
match action.as_deref() {
Some("skip_check") => {
self.function_depth += 1;
return Ok(());
}
Some("corrupt_depth") => {
self.function_depth = 0;
return Ok(());
}
_ => {}
}
Ok(())
});
if self.function_depth >= limits.max_function_depth {
return Err(LimitExceeded::MaxFunctionDepth(limits.max_function_depth));
}
self.function_depth += 1;
Ok(())
}
pub fn pop_function(&mut self) {
if self.function_depth > 0 {
self.function_depth -= 1;
}
}
pub fn push_subst(&mut self, limits: &ExecutionLimits) -> Result<(), LimitExceeded> {
if self.subst_depth >= limits.max_subst_depth {
return Err(LimitExceeded::MaxSubstDepth(limits.max_subst_depth));
}
self.subst_depth += 1;
Ok(())
}
pub fn pop_subst(&mut self) {
self.subst_depth = self.subst_depth.saturating_sub(1);
}
pub fn push_subshell(&mut self, limits: &ExecutionLimits) -> Result<(), LimitExceeded> {
if self.subshell_depth >= limits.max_subshell_depth {
return Err(LimitExceeded::MaxSubshellDepth(limits.max_subshell_depth));
}
self.subshell_depth += 1;
Ok(())
}
pub fn pop_subshell(&mut self) {
self.subshell_depth = self.subshell_depth.saturating_sub(1);
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum LimitExceeded {
#[error("execution budget exhausted: {0}")]
ExecutionBudget(ExecutionBudgetExceeded),
#[error("maximum command count exceeded ({0})")]
MaxCommands(usize),
#[error("maximum loop iterations exceeded ({0})")]
MaxLoopIterations(usize),
#[error("maximum total loop iterations exceeded ({0})")]
MaxTotalLoopIterations(usize),
#[error("maximum function depth exceeded ({0})")]
MaxFunctionDepth(usize),
#[error("maximum command substitution depth exceeded ({0})")]
MaxSubstDepth(usize),
#[error("maximum subshell depth exceeded ({0})")]
MaxSubshellDepth(usize),
#[error("maximum file descriptors exceeded ({0})")]
MaxFileDescriptors(usize),
#[error("execution timeout ({0:?})")]
Timeout(Duration),
#[error("parser timeout ({0:?})")]
ParserTimeout(Duration),
#[error("input too large ({0} bytes, max {1} bytes)")]
InputTooLarge(usize, usize),
#[error("AST nesting too deep ({0} levels, max {1})")]
AstTooDeep(usize, usize),
#[error("parser fuel exhausted ({0} operations, max {1})")]
ParserExhausted(usize, usize),
#[error("session command limit exceeded ({0} total commands)")]
SessionMaxCommands(u64),
#[error("session exec() call limit exceeded ({0} calls)")]
SessionMaxExecCalls(u64),
#[error("memory limit exceeded: {0}")]
Memory(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ExecutionBudgetExceeded {
#[error("work units ({used} used, max {limit})")]
WorkUnits { used: u64, limit: u64 },
#[error("aggregate input bytes ({used} used, max {limit})")]
InputBytes { used: u64, limit: u64 },
#[error("live intermediate bytes ({used} requested, max {limit})")]
LiveBytes { used: u64, limit: u64 },
#[error("deadline exceeded ({limit:?})")]
Deadline { limit: Duration },
#[error("cancelled")]
Cancelled,
#[error("request closed")]
RequestClosed,
}
#[derive(Debug)]
struct ExecutionBudgetInner {
max_work_units: u64,
max_input_bytes: u64,
max_live_bytes: u64,
timeout: Duration,
work_units: AtomicU64,
input_bytes: AtomicU64,
live_bytes: AtomicU64,
deadline: Option<Instant>,
cancelled: Arc<AtomicBool>,
poisoned: Mutex<Option<ExecutionBudgetExceeded>>,
closed: AtomicBool,
}
#[derive(Debug, Clone)]
pub struct ExecutionBudget {
inner: Arc<ExecutionBudgetInner>,
}
impl ExecutionBudget {
const DEADLINE_GRACE: Duration = Duration::from_millis(100);
pub fn new(limits: &ExecutionLimits, cancelled: Arc<AtomicBool>) -> Self {
Self {
inner: Arc::new(ExecutionBudgetInner {
max_work_units: limits.max_work_units,
max_input_bytes: limits.max_aggregate_input_bytes,
max_live_bytes: limits.max_live_intermediate_bytes,
timeout: limits.timeout,
work_units: AtomicU64::new(0),
input_bytes: AtomicU64::new(0),
live_bytes: AtomicU64::new(0),
deadline: Instant::now()
.checked_add(limits.timeout)
.and_then(|deadline| deadline.checked_add(Self::DEADLINE_GRACE)),
cancelled,
poisoned: Mutex::new(None),
closed: AtomicBool::new(false),
}),
}
}
fn failure(&self) -> Option<LimitExceeded> {
self.inner
.poisoned
.lock()
.expect("execution budget poison lock")
.clone()
.map(LimitExceeded::ExecutionBudget)
}
fn poison(&self, reason: ExecutionBudgetExceeded) -> LimitExceeded {
let mut poisoned = self
.inner
.poisoned
.lock()
.expect("execution budget poison lock");
let reason = poisoned.get_or_insert(reason).clone();
LimitExceeded::ExecutionBudget(reason)
}
pub fn check(&self) -> Result<(), LimitExceeded> {
if self.inner.closed.load(Ordering::Acquire) {
return Err(LimitExceeded::ExecutionBudget(
ExecutionBudgetExceeded::RequestClosed,
));
}
if let Some(err) = self.failure() {
return Err(err);
}
if self.inner.cancelled.load(Ordering::Relaxed) {
return Err(self.poison(ExecutionBudgetExceeded::Cancelled));
}
if self
.inner
.deadline
.is_some_and(|deadline| Instant::now() >= deadline)
{
return Err(self.poison(ExecutionBudgetExceeded::Deadline {
limit: self.inner.timeout,
}));
}
Ok(())
}
pub(crate) fn completion_guard(&self) -> ExecutionBudgetCompletionGuard {
ExecutionBudgetCompletionGuard {
budget: self.clone(),
}
}
fn close(&self) {
self.inner.closed.store(true, Ordering::Release);
}
#[cfg(all(
not(target_family = "wasm"),
any(
feature = "python",
feature = "typescript",
feature = "http_client",
feature = "scripted_tool"
)
))]
pub(crate) async fn run<F>(&self, future: F) -> Result<F::Output, LimitExceeded>
where
F: std::future::Future,
{
self.check()?;
tokio::pin!(future);
let mut cancellation_poll = tokio::time::interval(Duration::from_millis(10));
cancellation_poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
output = &mut future => {
self.check()?;
return Ok(output);
}
_ = cancellation_poll.tick() => self.check()?,
}
}
}
#[cfg(all(
target_family = "wasm",
any(
feature = "python",
feature = "typescript",
feature = "http_client",
feature = "scripted_tool"
)
))]
pub(crate) async fn run<F>(&self, future: F) -> Result<F::Output, LimitExceeded>
where
F: std::future::Future,
{
self.check()?;
let output = future.await;
self.check()?;
Ok(output)
}
pub fn consume_work(&self, units: u64) -> Result<(), LimitExceeded> {
self.check()?;
if let Err(used) = reserve_atomic(&self.inner.work_units, units, self.inner.max_work_units)
{
return Err(self.poison(ExecutionBudgetExceeded::WorkUnits {
used,
limit: self.inner.max_work_units,
}));
}
Ok(())
}
pub(crate) fn work_units(&self) -> u64 {
self.inner.work_units.load(Ordering::Relaxed)
}
pub fn consume_input(&self, bytes: usize) -> Result<(), LimitExceeded> {
self.check()?;
let bytes = u64::try_from(bytes).unwrap_or(u64::MAX);
if let Err(used) =
reserve_atomic(&self.inner.input_bytes, bytes, self.inner.max_input_bytes)
{
return Err(self.poison(ExecutionBudgetExceeded::InputBytes {
used,
limit: self.inner.max_input_bytes,
}));
}
Ok(())
}
pub fn lease_bytes(&self, bytes: usize) -> Result<ExecutionBudgetLease, LimitExceeded> {
self.check()?;
let bytes = u64::try_from(bytes).unwrap_or(u64::MAX);
if let Err(used) = self.reserve_live_bytes(bytes) {
return Err(self.poison(ExecutionBudgetExceeded::LiveBytes {
used,
limit: self.inner.max_live_bytes,
}));
}
Ok(ExecutionBudgetLease {
budget: self.clone(),
bytes,
})
}
fn reserve_live_bytes(&self, bytes: u64) -> Result<(), u64> {
reserve_atomic(&self.inner.live_bytes, bytes, self.inner.max_live_bytes).map(|_| ())
}
#[cfg(test)]
fn live_bytes_for_test(&self) -> u64 {
self.inner.live_bytes.load(Ordering::Relaxed)
}
}
fn reserve_atomic(counter: &AtomicU64, amount: u64, limit: u64) -> Result<u64, u64> {
let mut current = counter.load(Ordering::Relaxed);
loop {
let Some(next) = current.checked_add(amount) else {
return Err(u64::MAX);
};
if next > limit {
return Err(next);
}
match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => return Ok(next),
Err(observed) => current = observed,
}
}
}
#[derive(Debug)]
pub struct ExecutionBudgetLease {
budget: ExecutionBudget,
bytes: u64,
}
impl ExecutionBudgetLease {
#[cfg(feature = "jq")]
pub(crate) fn grow(&mut self, additional: usize) -> Result<(), LimitExceeded> {
self.budget.check()?;
let additional = u64::try_from(additional).unwrap_or(u64::MAX);
let previous = self
.budget
.inner
.live_bytes
.fetch_add(additional, Ordering::Relaxed);
let used = previous.saturating_add(additional);
if used > self.budget.inner.max_live_bytes || previous.checked_add(additional).is_none() {
self.budget
.inner
.live_bytes
.fetch_sub(additional, Ordering::Relaxed);
return Err(self.budget.poison(ExecutionBudgetExceeded::LiveBytes {
used,
limit: self.budget.inner.max_live_bytes,
}));
}
self.bytes = self.bytes.saturating_add(additional);
Ok(())
}
}
pub(crate) struct ExecutionBudgetCompletionGuard {
budget: ExecutionBudget,
}
impl Drop for ExecutionBudgetCompletionGuard {
fn drop(&mut self) {
self.budget.close();
}
}
impl ExecutionBudgetLease {
fn try_grow_to(&mut self, bytes: u64) -> Result<(), LimitExceeded> {
self.budget.check()?;
if bytes <= self.bytes {
return Ok(());
}
let additional = bytes - self.bytes;
if let Err(used) = self.budget.reserve_live_bytes(additional) {
return Err(self.budget.poison(ExecutionBudgetExceeded::LiveBytes {
used,
limit: self.budget.inner.max_live_bytes,
}));
}
self.bytes = bytes;
Ok(())
}
fn shrink_to(&mut self, bytes: u64) {
debug_assert!(bytes <= self.bytes);
let released = self.bytes - bytes;
self.budget
.inner
.live_bytes
.fetch_sub(released, Ordering::Relaxed);
self.bytes = bytes;
}
}
impl Drop for ExecutionBudgetLease {
fn drop(&mut self) {
self.budget
.inner
.live_bytes
.fetch_sub(self.bytes, Ordering::Relaxed);
}
}
#[derive(Debug)]
pub(crate) struct BudgetedVec<T> {
inner: Vec<T>,
lease: Option<ExecutionBudgetLease>,
}
pub(crate) type BudgetedBytes = BudgetedVec<u8>;
impl<T> BudgetedVec<T> {
pub(crate) fn new(budget: Option<&ExecutionBudget>) -> Result<Self, LimitExceeded> {
Ok(Self {
inner: Vec::new(),
lease: budget.map(|budget| budget.lease_bytes(0)).transpose()?,
})
}
pub(crate) fn try_with_capacity(
budget: Option<&ExecutionBudget>,
capacity: usize,
) -> Result<Self, LimitExceeded> {
let mut value = Self::new(budget)?;
value.try_reserve_capacity(capacity)?;
Ok(value)
}
fn capacity_bytes(capacity: usize) -> Result<u64, LimitExceeded> {
capacity
.checked_mul(std::mem::size_of::<T>())
.and_then(|bytes| u64::try_from(bytes).ok())
.ok_or_else(|| LimitExceeded::Memory("intermediate buffer size overflow".into()))
}
fn growth_capacity(&self, required: usize) -> Result<usize, LimitExceeded> {
if required <= self.inner.capacity() {
return Ok(self.inner.capacity());
}
self.inner
.capacity()
.checked_mul(2)
.map(|grown| grown.max(required))
.ok_or_else(|| LimitExceeded::Memory("intermediate buffer size overflow".into()))
}
fn try_reserve_capacity(&mut self, capacity: usize) -> Result<(), LimitExceeded> {
if capacity <= self.inner.capacity() {
if let Some(lease) = &self.lease {
lease.budget.check()?;
}
return Ok(());
}
let previous_bytes = self.lease.as_ref().map_or(0, |lease| lease.bytes);
let capacity_bytes = Self::capacity_bytes(capacity)?;
if let Some(lease) = &mut self.lease {
lease.try_grow_to(capacity_bytes)?;
}
let additional = capacity - self.inner.len();
if self.inner.try_reserve_exact(additional).is_err() {
if let Some(lease) = &mut self.lease {
lease.shrink_to(previous_bytes);
}
return Err(LimitExceeded::Memory(
"intermediate buffer allocation failed".into(),
));
}
Ok(())
}
pub(crate) fn try_push(&mut self, value: T) -> Result<(), LimitExceeded> {
let required = self
.inner
.len()
.checked_add(1)
.ok_or_else(|| LimitExceeded::Memory("intermediate buffer size overflow".into()))?;
let capacity = self.growth_capacity(required)?;
self.try_reserve_capacity(capacity)?;
self.inner.push(value);
Ok(())
}
pub(crate) fn into_parts(self) -> (Vec<T>, Option<ExecutionBudgetLease>) {
(self.inner, self.lease)
}
}
impl<T: Clone> BudgetedVec<T> {
pub(crate) fn try_extend_from_slice(&mut self, values: &[T]) -> Result<(), LimitExceeded> {
let required = self
.inner
.len()
.checked_add(values.len())
.ok_or_else(|| LimitExceeded::Memory("intermediate buffer size overflow".into()))?;
let capacity = self.growth_capacity(required)?;
self.try_reserve_capacity(capacity)?;
self.inner.extend_from_slice(values);
Ok(())
}
}
impl<T> std::ops::Deref for BudgetedVec<T> {
type Target = [T];
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> std::ops::DerefMut for BudgetedVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl std::io::Write for BudgetedVec<u8> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.try_extend_from_slice(buf)
.map_err(std::io::Error::other)?;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct BudgetedString {
inner: String,
lease: Option<ExecutionBudgetLease>,
}
impl BudgetedString {
pub(crate) fn new(budget: Option<&ExecutionBudget>) -> Result<Self, LimitExceeded> {
Ok(Self {
inner: String::new(),
lease: budget.map(|budget| budget.lease_bytes(0)).transpose()?,
})
}
fn growth_capacity(&self, required: usize) -> Result<usize, LimitExceeded> {
if required <= self.inner.capacity() {
return Ok(self.inner.capacity());
}
self.inner
.capacity()
.checked_mul(2)
.map(|grown| grown.max(required))
.ok_or_else(|| LimitExceeded::Memory("intermediate string size overflow".into()))
}
fn try_reserve_capacity(&mut self, capacity: usize) -> Result<(), LimitExceeded> {
if capacity <= self.inner.capacity() {
if let Some(lease) = &self.lease {
lease.budget.check()?;
}
return Ok(());
}
let capacity_bytes = u64::try_from(capacity)
.map_err(|_| LimitExceeded::Memory("intermediate string size overflow".into()))?;
let previous_bytes = self.lease.as_ref().map_or(0, |lease| lease.bytes);
if let Some(lease) = &mut self.lease {
lease.try_grow_to(capacity_bytes)?;
}
let additional = capacity - self.inner.len();
if self.inner.try_reserve_exact(additional).is_err() {
if let Some(lease) = &mut self.lease {
lease.shrink_to(previous_bytes);
}
return Err(LimitExceeded::Memory(
"intermediate string allocation failed".into(),
));
}
Ok(())
}
pub(crate) fn try_push_str(&mut self, value: &str) -> Result<(), LimitExceeded> {
let required = self
.inner
.len()
.checked_add(value.len())
.ok_or_else(|| LimitExceeded::Memory("intermediate string size overflow".into()))?;
let capacity = self.growth_capacity(required)?;
self.try_reserve_capacity(capacity)?;
self.inner.push_str(value);
Ok(())
}
pub(crate) fn try_push(&mut self, value: char) -> Result<(), LimitExceeded> {
let required = self
.inner
.len()
.checked_add(value.len_utf8())
.ok_or_else(|| LimitExceeded::Memory("intermediate string size overflow".into()))?;
let capacity = self.growth_capacity(required)?;
self.try_reserve_capacity(capacity)?;
self.inner.push(value);
Ok(())
}
pub(crate) fn into_parts(self) -> (String, Option<ExecutionBudgetLease>) {
(self.inner, self.lease)
}
pub(crate) fn into_inner(self) -> String {
self.inner
}
}
impl std::ops::Deref for BudgetedString {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
pub const DEFAULT_MAX_VARIABLE_COUNT: usize = 10_000;
pub const DEFAULT_MAX_TOTAL_VARIABLE_BYTES: usize = 10_000_000; pub const DEFAULT_MAX_ARRAY_ENTRIES: usize = 100_000;
pub const DEFAULT_MAX_FUNCTION_COUNT: usize = 1_000;
pub const DEFAULT_MAX_FUNCTION_BODY_BYTES: usize = 1_000_000;
#[derive(Debug, Clone)]
pub struct MemoryLimits {
pub max_variable_count: usize,
pub max_total_variable_bytes: usize,
pub max_array_entries: usize,
pub max_function_count: usize,
pub max_function_body_bytes: usize,
}
impl Default for MemoryLimits {
fn default() -> Self {
Self {
max_variable_count: DEFAULT_MAX_VARIABLE_COUNT,
max_total_variable_bytes: DEFAULT_MAX_TOTAL_VARIABLE_BYTES,
max_array_entries: DEFAULT_MAX_ARRAY_ENTRIES,
max_function_count: DEFAULT_MAX_FUNCTION_COUNT,
max_function_body_bytes: DEFAULT_MAX_FUNCTION_BODY_BYTES,
}
}
}
impl MemoryLimits {
pub fn new() -> Self {
Self::default()
}
pub fn max_variable_count(mut self, count: usize) -> Self {
self.max_variable_count = count;
self
}
pub fn max_total_variable_bytes(mut self, bytes: usize) -> Self {
self.max_total_variable_bytes = bytes;
self
}
pub fn max_array_entries(mut self, count: usize) -> Self {
self.max_array_entries = count;
self
}
pub fn max_function_count(mut self, count: usize) -> Self {
self.max_function_count = count;
self
}
pub fn max_function_body_bytes(mut self, bytes: usize) -> Self {
self.max_function_body_bytes = bytes;
self
}
pub fn unlimited() -> Self {
Self {
max_variable_count: usize::MAX,
max_total_variable_bytes: usize::MAX,
max_array_entries: usize::MAX,
max_function_count: usize::MAX,
max_function_body_bytes: usize::MAX,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryBudget {
pub variable_count: usize,
pub variable_bytes: usize,
pub array_entries: usize,
pub function_count: usize,
pub function_body_bytes: usize,
}
impl MemoryBudget {
pub fn check_variable_insert(
&self,
key_len: usize,
value_len: usize,
is_new: bool,
old_key_len: usize,
old_value_len: usize,
limits: &MemoryLimits,
) -> Result<(), LimitExceeded> {
if is_new && self.variable_count >= limits.max_variable_count {
return Err(LimitExceeded::Memory(format!(
"variable count limit ({}) exceeded",
limits.max_variable_count
)));
}
let new_bytes =
(self.variable_bytes + key_len + value_len).saturating_sub(old_key_len + old_value_len);
if new_bytes > limits.max_total_variable_bytes {
return Err(LimitExceeded::Memory(format!(
"variable byte limit ({}) exceeded",
limits.max_total_variable_bytes
)));
}
Ok(())
}
pub fn record_variable_insert(
&mut self,
key_len: usize,
value_len: usize,
is_new: bool,
old_key_len: usize,
old_value_len: usize,
) {
if is_new {
self.variable_count += 1;
}
self.variable_bytes =
(self.variable_bytes + key_len + value_len).saturating_sub(old_key_len + old_value_len);
}
pub fn record_variable_remove(&mut self, key_len: usize, value_len: usize) {
self.variable_count = self.variable_count.saturating_sub(1);
self.variable_bytes = self.variable_bytes.saturating_sub(key_len + value_len);
}
pub fn check_array_entries(
&self,
additional: usize,
limits: &MemoryLimits,
) -> Result<(), LimitExceeded> {
if self.array_entries + additional > limits.max_array_entries {
return Err(LimitExceeded::Memory(format!(
"array entry limit ({}) exceeded",
limits.max_array_entries
)));
}
Ok(())
}
pub fn record_array_insert(&mut self, added: usize) {
self.array_entries += added;
}
pub fn record_array_remove(&mut self, removed: usize) {
self.array_entries = self.array_entries.saturating_sub(removed);
}
pub fn check_function_insert(
&self,
body_bytes: usize,
is_new: bool,
old_body_bytes: usize,
limits: &MemoryLimits,
) -> Result<(), LimitExceeded> {
if is_new && self.function_count >= limits.max_function_count {
return Err(LimitExceeded::Memory(format!(
"function count limit ({}) exceeded",
limits.max_function_count
)));
}
let new_bytes = (self.function_body_bytes + body_bytes).saturating_sub(old_body_bytes);
if new_bytes > limits.max_function_body_bytes {
return Err(LimitExceeded::Memory(format!(
"function body byte limit ({}) exceeded",
limits.max_function_body_bytes
)));
}
Ok(())
}
pub fn record_function_insert(
&mut self,
body_bytes: usize,
is_new: bool,
old_body_bytes: usize,
) {
if is_new {
self.function_count += 1;
}
self.function_body_bytes =
(self.function_body_bytes + body_bytes).saturating_sub(old_body_bytes);
}
pub fn record_function_remove(&mut self, body_bytes: usize) {
self.function_count = self.function_count.saturating_sub(1);
self.function_body_bytes = self.function_body_bytes.saturating_sub(body_bytes);
}
pub fn recompute_from_state<F>(
variables: &std::collections::HashMap<String, String>,
arrays: &std::collections::HashMap<String, std::collections::HashMap<usize, String>>,
assoc_arrays: &std::collections::HashMap<String, std::collections::HashMap<String, String>>,
function_count: usize,
function_body_bytes: usize,
is_internal: F,
) -> Self
where
F: Fn(&str) -> bool,
{
let mut budget = Self::default();
for (k, v) in variables {
if !is_internal(k) {
budget.variable_count += 1;
budget.variable_bytes += k.len() + v.len();
}
}
for arr in arrays.values() {
budget.array_entries += arr.len();
}
for arr in assoc_arrays.values() {
budget.array_entries += arr.len();
}
budget.function_count = function_count;
budget.function_body_bytes = function_body_bytes;
budget
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_limits() {
let limits = ExecutionLimits::default();
assert_eq!(limits.max_work_units, 100_000_000);
assert_eq!(limits.max_aggregate_input_bytes, 100_000_000);
assert_eq!(limits.max_live_intermediate_bytes, 32_000_000);
assert_eq!(limits.max_commands, 10_000);
assert_eq!(limits.max_loop_iterations, 10_000);
assert_eq!(limits.max_total_loop_iterations, 1_000_000);
assert_eq!(limits.max_function_depth, 100);
assert_eq!(limits.timeout, Duration::from_secs(30));
assert_eq!(limits.parser_timeout, Duration::from_secs(5));
assert_eq!(limits.max_input_bytes, 10_000_000);
assert_eq!(limits.max_ast_depth, 100);
assert_eq!(limits.max_parser_operations, 100_000);
assert_eq!(limits.max_stdout_bytes, 1_048_576);
assert_eq!(limits.max_stderr_bytes, 1_048_576);
assert_eq!(limits.max_subst_depth, 32);
assert_eq!(limits.max_subshell_depth, 32);
assert_eq!(limits.max_history_entries, 1_000);
assert_eq!(limits.max_history_bytes, 1_048_576);
assert_eq!(limits.max_history_output_bytes, 1_048_576);
assert!(!limits.capture_final_env);
}
#[test]
fn test_builder_pattern() {
let limits = ExecutionLimits::new()
.max_commands(100)
.max_loop_iterations(50)
.max_function_depth(10)
.timeout(Duration::from_secs(5));
assert_eq!(limits.max_commands, 100);
assert_eq!(limits.max_loop_iterations, 50);
assert_eq!(limits.max_function_depth, 10);
assert_eq!(limits.timeout, Duration::from_secs(5));
}
#[test]
fn test_command_counter() {
let limits = ExecutionLimits::new().max_commands(5);
let mut counters = ExecutionCounters::new();
for _ in 0..5 {
assert!(counters.tick_command(&limits).is_ok());
}
assert!(matches!(
counters.tick_command(&limits),
Err(LimitExceeded::MaxCommands(5))
));
}
#[test]
fn test_command_counter_saturates_on_overflow() {
let limits = ExecutionLimits::new().max_commands(5);
let mut counters = ExecutionCounters::new();
counters.commands = usize::MAX;
counters.session_commands = u64::MAX;
assert!(matches!(
counters.tick_command(&limits),
Err(LimitExceeded::MaxCommands(5))
));
assert_eq!(counters.commands, usize::MAX);
assert_eq!(counters.session_commands, u64::MAX);
}
#[test]
fn test_exec_counter_saturates_on_overflow() {
let mut counters = ExecutionCounters::new();
counters.session_exec_calls = u64::MAX;
counters.tick_exec_call();
assert_eq!(counters.session_exec_calls, u64::MAX);
}
#[test]
fn test_loop_counter() {
let limits = ExecutionLimits::new().max_loop_iterations(3);
let mut counters = ExecutionCounters::new();
counters.enter_loop();
for _ in 0..3 {
assert!(counters.tick_loop(&limits).is_ok());
}
assert!(matches!(
counters.tick_loop(&limits),
Err(LimitExceeded::MaxLoopIterations(3))
));
counters.exit_loop();
counters.enter_loop();
assert!(counters.tick_loop(&limits).is_ok());
}
#[test]
fn test_total_loop_counter_accumulates() {
let limits = ExecutionLimits::new()
.max_loop_iterations(5)
.max_total_loop_iterations(8);
let mut counters = ExecutionCounters::new();
counters.enter_loop();
for _ in 0..5 {
assert!(counters.tick_loop(&limits).is_ok());
}
assert_eq!(counters.total_loop_iterations, 5);
counters.exit_loop();
counters.enter_loop();
assert_eq!(counters.loop_iterations.last().copied(), Some(0));
assert_eq!(counters.total_loop_iterations, 5);
assert!(counters.tick_loop(&limits).is_ok()); assert!(counters.tick_loop(&limits).is_ok()); assert!(counters.tick_loop(&limits).is_ok());
assert!(matches!(
counters.tick_loop(&limits),
Err(LimitExceeded::MaxTotalLoopIterations(8))
));
}
#[test]
fn test_nested_loops_track_independently() {
let limits = ExecutionLimits::new().max_loop_iterations(2);
let mut counters = ExecutionCounters::new();
counters.enter_loop();
assert!(counters.tick_loop(&limits).is_ok());
counters.enter_loop();
assert!(counters.tick_loop(&limits).is_ok()); assert!(counters.tick_loop(&limits).is_ok()); counters.exit_loop();
assert!(counters.tick_loop(&limits).is_ok()); assert!(matches!(
counters.tick_loop(&limits),
Err(LimitExceeded::MaxLoopIterations(2))
)); }
#[test]
fn test_function_depth() {
let limits = ExecutionLimits::new().max_function_depth(2);
let mut counters = ExecutionCounters::new();
assert!(counters.push_function(&limits).is_ok());
assert!(counters.push_function(&limits).is_ok());
assert!(matches!(
counters.push_function(&limits),
Err(LimitExceeded::MaxFunctionDepth(2))
));
counters.pop_function();
assert!(counters.push_function(&limits).is_ok());
}
#[test]
fn test_subshell_depth() {
let limits = ExecutionLimits::new().max_subshell_depth(2);
let mut counters = ExecutionCounters::new();
assert!(counters.push_subshell(&limits).is_ok());
assert!(counters.push_subshell(&limits).is_ok());
assert!(matches!(
counters.push_subshell(&limits),
Err(LimitExceeded::MaxSubshellDepth(2))
));
counters.pop_subshell();
assert!(counters.push_subshell(&limits).is_ok());
}
#[test]
fn test_reset_for_execution() {
let limits = ExecutionLimits::new().max_commands(5);
let mut counters = ExecutionCounters::new();
for _ in 0..5 {
counters.tick_command(&limits).unwrap();
}
assert!(counters.tick_command(&limits).is_err());
counters.loop_iterations = vec![42];
counters.total_loop_iterations = 999;
counters.function_depth = 3;
counters.subst_depth = 2;
counters.subshell_depth = 2;
counters.reset_for_execution();
assert_eq!(counters.commands, 0);
assert!(counters.loop_iterations.is_empty());
assert_eq!(counters.total_loop_iterations, 0);
assert_eq!(counters.function_depth, 0);
assert_eq!(counters.subst_depth, 0);
assert_eq!(counters.subshell_depth, 0);
assert!(counters.tick_command(&limits).is_ok());
}
#[test]
fn test_zero_limit_is_strict_policy() {
let limits = ExecutionLimits::cli()
.max_work_units(0)
.max_aggregate_input_bytes(0)
.max_live_intermediate_bytes(0)
.max_commands(0)
.max_loop_iterations(0)
.max_total_loop_iterations(0)
.max_function_depth(0)
.max_input_bytes(0)
.max_ast_depth(0)
.max_parser_operations(0)
.max_stdout_bytes(0)
.max_stderr_bytes(0)
.max_subst_depth(0)
.max_subshell_depth(0)
.max_file_descriptors(0)
.max_word_split_fields(0)
.max_word_split_bytes(0);
let defaults = ExecutionLimits::default();
assert_eq!(limits.max_work_units, 0);
assert_eq!(limits.max_aggregate_input_bytes, 0);
assert_eq!(limits.max_live_intermediate_bytes, 0);
assert_eq!(limits.max_commands, 0);
assert_eq!(limits.max_loop_iterations, 0);
assert_eq!(limits.max_total_loop_iterations, 0);
assert_eq!(limits.max_function_depth, 0);
assert_eq!(limits.max_input_bytes, 0);
assert_eq!(limits.max_ast_depth, 0);
assert_eq!(limits.max_parser_operations, 0);
assert_eq!(limits.max_stdout_bytes, 0);
assert_eq!(limits.max_stderr_bytes, 0);
assert_eq!(limits.max_subst_depth, 0);
assert_eq!(limits.max_subshell_depth, 0);
assert_eq!(limits.max_file_descriptors, 0);
assert_eq!(limits.max_word_split_fields, defaults.max_word_split_fields);
assert_eq!(limits.max_word_split_bytes, defaults.max_word_split_bytes);
}
#[test]
fn test_nonzero_limit_works() {
let limits = ExecutionLimits::default()
.max_work_units(11)
.max_aggregate_input_bytes(12)
.max_live_intermediate_bytes(13)
.max_commands(5)
.max_loop_iterations(7)
.max_total_loop_iterations(42)
.max_function_depth(3)
.max_input_bytes(1024)
.max_ast_depth(10)
.max_parser_operations(500)
.max_stdout_bytes(2048)
.max_stderr_bytes(4096)
.max_subst_depth(8)
.max_subshell_depth(6)
.max_file_descriptors(16)
.max_word_split_fields(17)
.max_word_split_bytes(18);
assert_eq!(limits.max_work_units, 11);
assert_eq!(limits.max_aggregate_input_bytes, 12);
assert_eq!(limits.max_live_intermediate_bytes, 13);
assert_eq!(limits.max_commands, 5);
assert_eq!(limits.max_loop_iterations, 7);
assert_eq!(limits.max_total_loop_iterations, 42);
assert_eq!(limits.max_function_depth, 3);
assert_eq!(limits.max_input_bytes, 1024);
assert_eq!(limits.max_ast_depth, 10);
assert_eq!(limits.max_parser_operations, 500);
assert_eq!(limits.max_stdout_bytes, 2048);
assert_eq!(limits.max_stderr_bytes, 4096);
assert_eq!(limits.max_subst_depth, 8);
assert_eq!(limits.max_subshell_depth, 6);
assert_eq!(limits.max_file_descriptors, 16);
assert_eq!(limits.max_word_split_fields, 17);
assert_eq!(limits.max_word_split_bytes, 18);
}
#[test]
fn test_session_limits_zero_is_strict_policy() {
let limits = SessionLimits::unlimited()
.max_total_commands(0)
.max_exec_calls(0);
assert_eq!(limits.max_total_commands, 0);
assert_eq!(limits.max_exec_calls, 0);
}
#[test]
fn test_memory_limits_zero_is_strict_policy() {
let limits = MemoryLimits::unlimited()
.max_variable_count(0)
.max_total_variable_bytes(0)
.max_array_entries(0)
.max_function_count(0)
.max_function_body_bytes(0);
assert_eq!(limits.max_variable_count, 0);
assert_eq!(limits.max_total_variable_bytes, 0);
assert_eq!(limits.max_array_entries, 0);
assert_eq!(limits.max_function_count, 0);
assert_eq!(limits.max_function_body_bytes, 0);
}
#[test]
fn execution_budget_poison_is_shared_and_non_resettable() {
let limits = ExecutionLimits::new()
.max_work_units(2)
.max_aggregate_input_bytes(4);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let descendant = budget.clone();
budget.consume_work(2).unwrap();
let first = descendant.consume_work(1).unwrap_err();
assert!(matches!(
first,
LimitExceeded::ExecutionBudget(ExecutionBudgetExceeded::WorkUnits { .. })
));
assert_eq!(
budget.consume_input(1).unwrap_err().to_string(),
first.to_string()
);
}
#[test]
fn execution_budget_live_leases_release_but_exhaustion_poisons() {
let limits = ExecutionLimits::new().max_live_intermediate_bytes(4);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let lease = budget.lease_bytes(4).unwrap();
let err = budget.lease_bytes(1).unwrap_err();
assert!(matches!(
err,
LimitExceeded::ExecutionBudget(ExecutionBudgetExceeded::LiveBytes { .. })
));
drop(lease);
assert!(
budget.lease_bytes(1).is_err(),
"poison must survive lease release"
);
}
#[test]
fn execution_budget_observes_shared_cancellation() {
let cancelled = Arc::new(AtomicBool::new(false));
let budget = ExecutionBudget::new(&ExecutionLimits::new(), cancelled.clone());
cancelled.store(true, Ordering::Relaxed);
assert!(matches!(
budget.check(),
Err(LimitExceeded::ExecutionBudget(
ExecutionBudgetExceeded::Cancelled
))
));
}
#[test]
fn request_completion_guard_closes_during_unwind() {
let budget =
ExecutionBudget::new(&ExecutionLimits::new(), Arc::new(AtomicBool::new(false)));
let unwind = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _completion = budget.completion_guard();
panic!("adversarial teardown");
}));
assert!(unwind.is_err());
assert!(matches!(
budget.check(),
Err(LimitExceeded::ExecutionBudget(
ExecutionBudgetExceeded::RequestClosed
))
));
}
#[test]
fn execution_budget_counter_overflow_fails_without_wrapping() {
let limits = ExecutionLimits::new().max_work_units(u64::MAX);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
budget.consume_work(u64::MAX).unwrap();
assert!(matches!(
budget.consume_work(1),
Err(LimitExceeded::ExecutionBudget(
ExecutionBudgetExceeded::WorkUnits {
used: u64::MAX,
limit: u64::MAX
}
))
));
}
#[test]
fn request_lease_releases_charge_on_drop() {
let limits = ExecutionLimits::new().max_live_intermediate_bytes(4);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
drop(budget.lease_bytes(4).unwrap());
let replacement = budget.lease_bytes(4);
assert!(replacement.is_ok(), "released charge must be reusable");
}
#[test]
fn budgeted_vec_charges_before_growth_at_exact_boundary() {
let limits = ExecutionLimits::new().max_live_intermediate_bytes(4);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut bytes = BudgetedVec::new(Some(&budget)).unwrap();
bytes.try_extend_from_slice(b"1234").unwrap();
assert_eq!(&*bytes, b"1234");
assert!(matches!(
bytes.try_push(b'5'),
Err(LimitExceeded::ExecutionBudget(
ExecutionBudgetExceeded::LiveBytes { used: 8, limit: 4 }
))
));
assert_eq!(&*bytes, b"1234", "failed growth must not mutate");
}
#[test]
fn budgeted_vec_rejects_element_size_overflow_without_allocating() {
let limits = ExecutionLimits::new().max_live_intermediate_bytes(u64::MAX);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let result = BudgetedVec::<u64>::try_with_capacity(Some(&budget), usize::MAX);
assert!(matches!(result, Err(LimitExceeded::Memory(_))));
assert_eq!(budget.live_bytes_for_test(), 0);
}
#[test]
fn budgeted_vec_rolls_back_charge_when_allocator_rejects_reserve() {
let limits = ExecutionLimits::new().max_live_intermediate_bytes(u64::MAX);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let result = BudgetedVec::<u8>::try_with_capacity(Some(&budget), usize::MAX);
assert!(matches!(result, Err(LimitExceeded::Memory(_))));
assert_eq!(budget.live_bytes_for_test(), 0);
assert!(
budget.lease_bytes(1).is_ok(),
"allocation failure must not poison"
);
}
#[test]
fn budgeted_builders_release_on_drop_and_cancelled_growth() {
let cancelled = Arc::new(AtomicBool::new(false));
let limits = ExecutionLimits::new().max_live_intermediate_bytes(8);
let budget = ExecutionBudget::new(&limits, cancelled.clone());
let mut text = BudgetedString::new(Some(&budget)).unwrap();
text.try_push_str("1234").unwrap();
assert_eq!(budget.live_bytes_for_test(), 4);
cancelled.store(true, Ordering::Relaxed);
assert!(matches!(
text.try_push_str("5"),
Err(LimitExceeded::ExecutionBudget(
ExecutionBudgetExceeded::Cancelled
))
));
assert_eq!(budget.live_bytes_for_test(), 4);
drop(text);
assert_eq!(budget.live_bytes_for_test(), 0);
}
#[test]
fn nested_budgeted_builders_share_aggregate_live_bytes() {
let limits = ExecutionLimits::new().max_live_intermediate_bytes(8);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut outer = BudgetedVec::new(Some(&budget)).unwrap();
let mut inner = BudgetedString::new(Some(&budget)).unwrap();
outer.try_extend_from_slice(b"1234").unwrap();
inner.try_push_str("5678").unwrap();
assert_eq!(budget.live_bytes_for_test(), 8);
assert!(inner.try_push('9').is_err());
assert_eq!(budget.live_bytes_for_test(), 8);
drop((outer, inner));
assert_eq!(budget.live_bytes_for_test(), 0);
}
#[test]
fn concurrent_leases_cannot_overcommit_live_budget() {
let limits = ExecutionLimits::new().max_live_intermediate_bytes(8);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let barrier = Arc::new(std::sync::Barrier::new(3));
let mut handles = Vec::new();
for _ in 0..2 {
let budget = budget.clone();
let barrier = barrier.clone();
handles.push(std::thread::spawn(move || {
barrier.wait();
let lease = budget.lease_bytes(8);
barrier.wait();
lease
}));
}
barrier.wait();
barrier.wait();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
drop(results);
assert_eq!(budget.live_bytes_for_test(), 0);
}
}