use std::time::Duration;
#[cfg(feature = "failpoints")]
use fail::fail_point;
#[derive(Debug, Clone)]
pub struct ExecutionLimits {
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 capture_final_env: bool,
}
impl Default for ExecutionLimits {
fn default() -> Self {
Self {
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,
capture_final_env: false,
}
}
}
impl ExecutionLimits {
pub fn new() -> Self {
Self::default()
}
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, ..Self::default()
}
}
pub fn max_commands(mut self, count: usize) -> Self {
if count > 0 {
self.max_commands = count;
}
self
}
pub fn max_loop_iterations(mut self, count: usize) -> Self {
if count > 0 {
self.max_loop_iterations = count;
}
self
}
pub fn max_total_loop_iterations(mut self, count: usize) -> Self {
if count > 0 {
self.max_total_loop_iterations = count;
}
self
}
pub fn max_function_depth(mut self, depth: usize) -> Self {
if depth > 0 {
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 {
if bytes > 0 {
self.max_input_bytes = bytes;
}
self
}
pub fn max_ast_depth(mut self, depth: usize) -> Self {
if depth > 0 {
self.max_ast_depth = depth;
}
self
}
pub fn max_parser_operations(mut self, ops: usize) -> Self {
if ops > 0 {
self.max_parser_operations = ops;
}
self
}
pub fn max_stdout_bytes(mut self, bytes: usize) -> Self {
if bytes > 0 {
self.max_stdout_bytes = bytes;
}
self
}
pub fn max_stderr_bytes(mut self, bytes: usize) -> Self {
if bytes > 0 {
self.max_stderr_bytes = bytes;
}
self
}
pub fn max_subst_depth(mut self, depth: usize) -> Self {
if depth > 0 {
self.max_subst_depth = depth;
}
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 {
if count > 0 {
self.max_total_commands = count;
}
self
}
pub fn max_exec_calls(mut self, count: u64) -> Self {
if count > 0 {
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 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;
}
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 += 1;
self.session_commands += 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 += 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);
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum LimitExceeded {
#[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("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),
}
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 {
if count > 0 {
self.max_variable_count = count;
}
self
}
pub fn max_total_variable_bytes(mut self, bytes: usize) -> Self {
if bytes > 0 {
self.max_total_variable_bytes = bytes;
}
self
}
pub fn max_array_entries(mut self, count: usize) -> Self {
if count > 0 {
self.max_array_entries = count;
}
self
}
pub fn max_function_count(mut self, count: usize) -> Self {
if count > 0 {
self.max_function_count = count;
}
self
}
pub fn max_function_body_bytes(mut self, bytes: usize) -> Self {
if bytes > 0 {
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 - 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_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!(!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_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_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.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!(counters.tick_command(&limits).is_ok());
}
#[test]
fn test_zero_limit_uses_default() {
let defaults = ExecutionLimits::default();
let limits = ExecutionLimits::default()
.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);
assert_eq!(limits.max_commands, defaults.max_commands);
assert_eq!(limits.max_loop_iterations, defaults.max_loop_iterations);
assert_eq!(
limits.max_total_loop_iterations,
defaults.max_total_loop_iterations
);
assert_eq!(limits.max_function_depth, defaults.max_function_depth);
assert_eq!(limits.max_input_bytes, defaults.max_input_bytes);
assert_eq!(limits.max_ast_depth, defaults.max_ast_depth);
assert_eq!(limits.max_parser_operations, defaults.max_parser_operations);
assert_eq!(limits.max_stdout_bytes, defaults.max_stdout_bytes);
assert_eq!(limits.max_stderr_bytes, defaults.max_stderr_bytes);
assert_eq!(limits.max_subst_depth, defaults.max_subst_depth);
}
#[test]
fn test_nonzero_limit_works() {
let limits = ExecutionLimits::default()
.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);
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);
}
#[test]
fn test_session_limits_zero_uses_default() {
let defaults = SessionLimits::default();
let limits = SessionLimits::default()
.max_total_commands(0)
.max_exec_calls(0);
assert_eq!(limits.max_total_commands, defaults.max_total_commands);
assert_eq!(limits.max_exec_calls, defaults.max_exec_calls);
}
#[test]
fn test_memory_limits_zero_uses_default() {
let defaults = MemoryLimits::default();
let limits = MemoryLimits::default()
.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, defaults.max_variable_count);
assert_eq!(
limits.max_total_variable_bytes,
defaults.max_total_variable_bytes
);
assert_eq!(limits.max_array_entries, defaults.max_array_entries);
assert_eq!(limits.max_function_count, defaults.max_function_count);
assert_eq!(
limits.max_function_body_bytes,
defaults.max_function_body_bytes
);
}
}