JustBash.Limit (JustBash v0.4.0)
View SourceProduction resource limits for JustBash execution.
Prevents untrusted scripts from exhausting memory or CPU by enforcing hard caps on computation steps, output size, file size, value size, regex patterns, execution nesting depth, and elapsed wall clock time.
Usage
# Default limits (recommended for production)
bash = JustBash.new()
# Preset profiles
bash = JustBash.new(limits: :strict)
# Custom limits (merged with defaults)
bash = JustBash.new(limits: [max_steps: 50_000])
# No limits (not recommended for untrusted input)
bash = JustBash.new(limits: false)Bounds
| key | :strict | :default | :relaxed |
|---|---|---|---|
:max_steps | 10_000 | 100_000 | 1_000_000 |
:max_output_bytes | 65_536 | 1_048_576 | 10_485_760 |
:max_file_bytes | 65_536 | 1_048_576 | 10_485_760 |
:max_value_bytes | 65_536 | 1_048_576 | 10_485_760 |
:max_regex_pattern_bytes | 10_000 | 10_000 | 10_000 |
:max_exec_depth | 128 | 128 | 128 |
:max_wall_ms | 1_000 | 5_000 | 30_000 |
Every bound but the last counts work. :max_wall_ms bounds elapsed time for
a single JustBash.exec/2, which is the only bound that catches a script
that burns wall clock without doing countable work — the shape both of this
project's historical hangs took. Nested eval/source run inside the
top-level call's budget rather than starting a fresh one.
:max_steps does double duty: a whole word is one step no matter what it
expands into, so it is also the cap on how many words a single word may
expand into — see check_expansion_words!/2.
:max_value_bytes bounds a single expanded value. A step like
v=${v}${v} doubles a binary with no other bound seeing inside it, and
${v//a/$r} can grow a value by the product of two already-capped
binaries, so without this cap the wall clock cannot fire until that
allocation finishes. Call concat!/3 or replace!/5 (or
check_value_size!/2 with a byte count) before the memory is spent, the
way check_expansion_words!/2 is called as a word list is produced.
Summary
Functions
Raise ExceededError if an armed deadline has passed.
Bound how many words one word may expand into. Raises ExceededError if exceeded.
Check file data size before writing. Raises ExceededError if too large.
Check regex pattern size only. Use compile_regex/3 when you also need compilation.
Check a value's size before keeping it. Raises ExceededError if too large.
Check regex pattern size and compile. Raises ExceededError if pattern too large.
Concatenate two binaries, raising ExceededError if the result would
exceed :max_value_bytes.
Arm a deadline for one top-level execution, or nil when limits are off.
Returns the default limits.
Wrap an enumerable so each element it yields first checks deadline.
Build limits from a preset atom, keyword list, or false to disable.
Replace matches of regex in str with replacement, raising
ExceededError if the result would exceed :max_value_bytes.
Increment step counter. Raises ExceededError if limit is reached.
Increment exec depth and track the high-water mark. Raises ExceededError if limit is exceeded.
Track output bytes. Raises ExceededError if limit is reached.
Types
@type t() :: %JustBash.Limit{ max_exec_depth: pos_integer(), max_file_bytes: pos_integer(), max_output_bytes: pos_integer(), max_regex_pattern_bytes: pos_integer(), max_steps: pos_integer(), max_value_bytes: pos_integer(), max_wall_ms: pos_integer() }
Functions
@spec check_deadline!(JustBash.Limit.Deadline.t() | JustBash.t() | nil) :: :ok
Raise ExceededError if an armed deadline has passed.
Accepts a Deadline, a JustBash struct carrying one, or nil for "no
bound". Counting limits cannot see a loop that burns time without doing
countable work, which is how both of this project's historical hangs escaped
every other bound.
@spec check_expansion_words!(JustBash.t(), non_neg_integer()) :: :ok
Bound how many words one word may expand into. Raises ExceededError if exceeded.
The step counter cannot see this: a word is a single step regardless of the
size of the list it names, so {1..1000000} — twelve characters — is one
step and a million words. That is counted work, not merely slow work, so the
bound is :max_steps rather than the wall clock; the clock is checked
alongside it so a budget large enough to permit the list still cannot be
spent entirely on building it.
Call this with the running count as the list is produced, not with the finished list's length — the point is to refuse before the memory is spent.
@spec check_file_size!(JustBash.t(), String.t() | non_neg_integer()) :: :ok
Check file data size before writing. Raises ExceededError if too large.
Accepts the data binary, or a byte count for append-style writes where the resulting size is known without materializing the content.
Check regex pattern size only. Use compile_regex/3 when you also need compilation.
For call sites that need custom compilation logic (e.g. grep's flag handling, sed's BRE-to-ERE conversion), call this directly.
@spec check_value_size!(JustBash.t(), String.t() | non_neg_integer()) :: :ok
Check a value's size before keeping it. Raises ExceededError if too large.
Accepts the data binary, or a byte count so callers can refuse a
concatenation or replacement before it is built — see concat!/3 and
replace!/5.
@spec compile_regex(t() | nil, String.t(), String.t() | [atom()]) :: {:ok, Regex.t()} | {:error, term()}
Check regex pattern size and compile. Raises ExceededError if pattern too large.
Centralizes the check-then-compile pattern used across commands (grep, sed, awk, jq, etc.). Accepts a limits struct (not a full bash struct) so it can be called from command internals that don't carry the full struct.
@spec concat!(JustBash.t(), binary(), binary()) :: binary()
Concatenate two binaries, raising ExceededError if the result would
exceed :max_value_bytes.
The check is on the sum of the sizes, so the oversized result is never
allocated — a single v=${v}${v} of a multi-gigabyte value is the hole
this closes. Call this as word parts are joined, not with the finished
binary.
@spec deadline(t() | nil) :: JustBash.Limit.Deadline.t() | nil
Arm a deadline for one top-level execution, or nil when limits are off.
Call this once per JustBash.exec/2 and carry the result; check_deadline!/1
then costs a single monotonic clock read and an integer comparison, so it is
affordable on the interpreter's statement loop.
@spec defaults() :: t()
Returns the default limits.
@spec enforce_deadline(Enumerable.t(), JustBash.Limit.Deadline.t() | nil) :: Enumerable.t()
Wrap an enumerable so each element it yields first checks deadline.
Used by JustBash.Commands.Seq: a whole command is a single step as far as
the step counter is concerned, so without this seq 1 100000000 is
unbounded. Commands whose loop is not already an enumerable call
check_deadline!/1 directly instead — see JustBash.Commands.Find.
Build limits from a preset atom, keyword list, or false to disable.
Replace matches of regex in str with replacement, raising
ExceededError if the result would exceed :max_value_bytes.
The check is on the projected size from match lengths, so the oversized
result is never allocated — a single ${v//a/$r} of cap-sized v and
r is the hole this closes. Call this instead of Regex.replace/4.
@spec step!(JustBash.t()) :: JustBash.t()
Increment step counter. Raises ExceededError if limit is reached.
@spec track_exec_depth!(JustBash.t()) :: JustBash.t()
Increment exec depth and track the high-water mark. Raises ExceededError if limit is exceeded.
@spec track_output!(JustBash.t(), non_neg_integer()) :: JustBash.t()
Track output bytes. Raises ExceededError if limit is reached.