Skip to content

symbolic

Symbolic math over untrusted strings: restricted parsing + isolated evaluation.

Two hazards drive every design choice here, both measured rather than assumed:

  1. sympy.sympify on a caller-supplied string is remote code execution -- sympify("__import__('os').getcwd()") returns the working directory. So is parse_expr called WITHOUT an explicit namespace. Agents pass strings that can originate in feed content, so parse_safe is the only entry point and it pins both local_dict and global_dict.
  2. 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, inside parse_expr itself, so parse_safe('1e300000') costs 7.1s and builds a 300,002-char number before evaluation ever starts -- _screen rejects 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.