symbolic
Symbolic math over untrusted strings: restricted parsing + isolated evaluation.
Two hazards drive every design choice here, both measured rather than assumed:
sympy.sympifyon a caller-supplied string is remote code execution --sympify("__import__('os').getcwd()")returns the working directory. So isparse_exprcalled WITHOUT an explicit namespace. Agents pass strings that can originate in feed content, soparse_safeis the only entry point and it pins bothlocal_dictandglobal_dict.- Small inputs can produce unbounded work:
expand((x+1)**2000)yields 887KB in 1.4s. Every EVALUATION therefore runs in a subprocess with a wall-clock timeout and an address-space cap, and results are truncated before return. Numeric-literal PARSING is a separate hazard with its own bound: it happens outside that subprocess, insideparse_expritself, soparse_safe('1e300000')costs 7.1s and builds a 300,002-char number before evaluation ever starts --_screenrejects literals above a digit/exponent ceiling for exactly this reason, cheaply, before parse_expr runs.
A subprocess is the only bound that holds against unbounded EVALUATION -- SIGALRM cannot preempt the C-level loops inside SymPy. Pathological literals are bounded a different way, by refusing them before parsing.
parse_safe(text)
Parse a caller-supplied expression with no access to builtins or imports.
Two layers, both required. _screen blocks attribute-access escapes that a
restricted namespace cannot, and bounds pathological numeric literals (see
_check_literal_magnitude); local_dict + global_dict block name lookup.
Passing only transformations= leaves parse_expr's default namespace, which
IS exploitable -- verified: it returns the working directory for
__import__('os').getcwd() and reads /etc/passwd.
Contract: this function is bounded against pathological LITERALS by
_screen, which runs before any SymPy parsing work -- but that bound
covers literal MAGNITUDE only, not evaluator cost. Parsing an ordinary-
looking expression can still hang: 9**9**9 runs past 60s inside
parse_expr(evaluate=True), before any subprocess exists to bound it.
Callers must route parse_safe (and anything downstream of it) through
run_isolated rather than assuming this function returns quickly.
It is NOT bounded against expensive EVALUATION either -- simplify,
integrate, expand, and friends can still do unbounded work on a
perfectly ordinary-looking parsed expression (e.g. expand((x+1)**2000)).
That cost belongs inside run_isolated, which is the only boundary with a
wall-clock timeout and a memory cap.
run_isolated(fn_name, payload, timeout=DEFAULT_TIMEOUT)
Run symbolic_ops.<fn_name>(payload) in a subprocess, killed at timeout.
Costs ~0.3s of process spawn per call -- acceptable for correctness-critical tools an agent calls occasionally, and it is the only bound that holds.
truncate(text)
Cap a result so an 887KB expansion cannot flood the caller's context.