v. vanilla 2.0
THE VANILLA HANDBOOK / 2.0

Security & limits

Feature permissions, resource budgets and trust boundaries.

This reference describes the implemented native runtime. Historical paths and internal version labels are retained where relevant. Later extension chapters supersede older limitations.

This is a small interpreter with defensive validation, not a hardened sandbox. The limits below prevent common runaway scripts and parser/ownership failures; they do not promise a strict CPU, RSS, or wall-clock budget. Use OS process isolation, permissions, and external timeouts for untrusted workloads.

TCP connect/listen is now available; see SOCKETS.md for all socket limits and precise EOF/deadline semantics. --allow-network permits plaintext TCP and verified TLS clients to numeric addresses or DNS names, as well as existing HTTP/S. --allow-listen additionally authorizes explicit listener creation; it is not a firewall or sandbox. Empty listen address explicitly means IPv4 loopback, not wildcard. Hostname resolution uses asynchronous c-ares with bounded candidates, query/send/event work and one deadline shared with candidate connects and TLS handshake. Socket resource budgets are fatal and independent of HTTP budgets. TLS validates the original requested hostname/IP and certificate chain, not a DNS-provided canonical identity. DNS is unauthenticated. --tls-ca-file replaces default socket trust, --dns-server replaces socket resolver configuration; both are explicit operator choices requiring the network gate. Neither is a script-level bypass. Trust/configuration file I/O, library allocations, kernel buffers, crypto processing and scheduling are not hard RSS/real-time bounds. TLS transport errors/timeouts invalidate the handle; there is no insecure mode or plaintext fallback. System/default OpenSSL configuration and inherited trust environment remain operator-controlled.

Fixed limits

Utility-specific bounds are in UTILITIES.md; the added SQLite, CSV/date, snapshot/function-copy and test-isolation bounds/trust policy are in WORKLOG.md. --allow-db is a separate trusted-script gate, not filesystem confinement. Fatal resource errors are explicitly distinguished from recoverable runtime errors; try/catch cannot reset or evade budgets.

Resource Limit / interpretation
Executed instructions Default 1,000,000 across all function calls; CLI --max-steps N accepts 1..100,000,000. Each executed code line consumes one step, including control lines.
Active calls 64, including main.
Source bytes 4 MiB aggregate across all source loads, including repeated includes.
Source lines 100,000 aggregate, including blank/comment lines.
Source line bytes 65,536 excluding LF (CR counts).
Tokens 1,024 per line, including command.
Function count 4,096; declared arguments 0..1,024 (the token budget can impose a smaller callable limit).
Active include files 32; canonical-path cycle detection.
Lexical block nesting 128.
Expression bytes 16,384 after tokens are joined.
Expression parser depth 128 nested parser calls; includes parentheses, unary operators, powers, and function arguments.
Identifier bytes 128; ASCII letters/underscore followed by letters/digits/underscore.
Local variables 4,096 per invocation; redeclaration does not grow the map.
Scalar/expanded string 1 MiB, including concatenation, replace/join, return values, and file reads.
Array items 16,384.
Per-frame stored data 2 MiB accounted by variable-name bytes, scalar/item string capacities, array vector capacity times sizeof(std::string), and conservative structured JSON storage. Includes retained array capacity after pops, positional arguments and active foreach snapshots; not an exact RSS cap.
Per-call arguments 2 MiB accounted typed value storage, checked while collecting and on entry; retained arguments also count against frame storage.
Environment overlay 4,096 changed keys, 2 MiB total key/value bytes; inherited environment is external input.
Stdout 16 MiB total per run; each individual concatenated print is also limited to 1 MiB.
File writes 16 MiB aggregate content bytes written/appended/copied per run; each text value at most 1 MiB. Copies use bounded chunks. Empty writes still consume instructions, but no byte budget.
CLI arguments 1024 items, 1 MiB aggregate bytes.
Stdin / streaming files 64 KiB per line; 16 MiB cumulative consumed bytes for stdin and a separate shared 16 MiB streaming-file budget.
File readers 32 open script-wide handles; released on explicit close or interpreter destruction.
JSON 1 MiB input, output and conservative structured storage each; depth 64, nodes 16384; also charged to frame budget.
Directory listing 16384 entries and 1 MiB accounted name data.
HTTP URL 8192 bytes; total timeout 1..30000 ms; body cap 1..1 MiB; headers 64 KiB/call; 64 requests and 60000 ms summed requested timeout reservations per run; 16 MiB aggregate request/response bodies.
Random integers Signed 32-bit inclusive bounds; seed is unsigned 32-bit.

Every integer conversion is checked for integrality and range before casting. Every arithmetic operation is checked for finite results. All integer bounds passed to the conversion helper are within the exactly representable binary64 integer range. Array indices and substring starts are checked before access. Strings/arrays use value ownership, including aliasing source/destination operations. Data accumulation during concatenation, array creation, split, replacement, and call argument collection is bounded before large fan-out can accumulate.

A block's structural target and nearest enclosing loop are computed before execution, not found by unchecked runtime scanning. All functions are structurally validated before main starts, even unused functions and unreachable branches. The runtime validates data-dependent types, references, numeric expressions, and limits at the executing source location. Parser recursion is separately bounded from script recursion.

Files and includes

--allow-files controls all file builtins. Source reads/includes work without it and are bounded as source input. File paths are not restricted to a root. Legacy and streaming reads follow symlinks; copy/rename reject a symlink source and atomically refuse existing destinations. Parent directories may still be symlinks. Canonical include paths catch ordinary symlink cycles, while depth/aggregate limits bound other repeated source loads. Relative file paths use CWD; relative includes use the including source's directory.

Read/write operations check regular-file status to avoid ordinary device/FIFO misuse. These standard-library status checks and stream opens are not atomic: a concurrently changed path can race the check, and a hostile/network filesystem can block or behave unexpectedly. No claim of race-proof path security, no process jail, no filesystem transaction, no guaranteed wall-clock deadline. Writes can truncate existing files, and successful prior effects are not rolled back when a later instruction fails. The Python tests impose their own subprocess timeout; the CLI does not enforce wall time.

HTTP/S is separately gated by --allow-network, which permits private networks and arbitrary reachable destinations and is not SSRF prevention. Redirects/proxies are disabled; protocols are HTTP/S only, TLS peer and hostname verification always enabled, transfer time/body/header/request budgets enforced through libcurl. There are no shell execution or DLL extension paths. The separate CGI gateway has its own operator configuration and worker supervision; see Apache CGI. Environment get can expose inherited secrets to stdout or an opted-in HTTP request; run with an appropriate environment. Environment set remains a bounded script-local overlay shared across calls, not putenv with borrowed temporary storage.

Build/testing choices

Release and sanitizer binaries are separate. ASan and UBSan run together with leak detection and halt-on-error options; sanitizer builds use non-PIE linking for predictable shadow-memory placement in container environments. Passing tests demonstrate the exercised behaviors, not proof against all malformed input or undefined behavior. No fuzzing-campaign or benchmark claim is made.