| Version | Supported |
|---|---|
| 0.5.x | ✅ |
| 0.4.x | ✅ |
| 0.3.x | ❌ |
| 0.2.x | ❌ |
| 0.1.x | ❌ |
If you discover a security vulnerability in Geno, please report it responsibly:
- Do NOT open a public GitHub issue for security vulnerabilities.
- Use GitHub private vulnerability reporting.
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
You can expect:
- Acknowledgment within 48 hours
- Status update within 7 days
- Credit in the security advisory (unless you prefer anonymity)
Geno's security boundary depends on how code is executed:
| Surface | Isolation boundary | Guidance |
|---|---|---|
geno run and the hosted server |
Sandboxed child process | Preferred built-in boundary for untrusted Geno source |
geno.sandbox.run_sandboxed() |
ProcessSandbox child process by default |
Supports hard time and resource limits |
geno.api.run() |
In-process sandbox with cooperative limits | Embedding API; use caller-managed process isolation for untrusted code |
| Generated JavaScript | No sandbox or resource isolation | Trusted-code deployments only |
Capability grants permit specific effects; they do not turn generated JavaScript or an in-process host into an operating-system security boundary. See Execution Surface for the full matrix.
Geno is designed to safely execute code generated by LLMs. The threat model assumes:
- Untrusted Input: Code may be adversarial or contain sandbox escape attempts
- Local Execution: Code runs on your machine, so escapes could be damaging
- Defense in Depth: Multiple layers prevent any single vulnerability from being catastrophic
- Side-channel attacks (timing, cache, etc.)
- Attacks requiring physical access to the machine
- Vulnerabilities in Python itself or the OS kernel
- Compiled code executed outside the Geno runtime
- The benchmark runner's raw Python solution executor, which is a developer convenience for research comparisons and not a supported production sandbox for untrusted Python
Geno implements defense in depth with three layers:
- Static Validation - Code is analyzed before execution to detect dangerous patterns
- Runtime Sandbox - Execution occurs with restricted builtins and safe wrappers
- Process Isolation - Used by process-sandboxed surfaces for hard resource limits
Before any code executes, validate_code_safety() scans for:
- Dangerous imports (
os,sys,subprocess,socket, etc.) - Blocked builtins (
eval,exec,compile,__import__, etc.) - Dangerous attribute access (
__class__,__bases__,__globals__, etc.) - Private attribute access (
_evaluate,_internal, etc.) that may bypass runtime wrappers - Sandbox escape patterns
In strict mode (default), code with any warnings is rejected.
The sandbox provides:
Blocked Builtins:
eval,exec,compile- Code executionopen- File system accessinput- User input (can hang)__import__- Module importsglobals,locals,vars- Namespace introspectionbreakpoint,help,license- Interactive functionsmemoryview- Low-level memory accesssetattr,delattr- Object mutation
Safe Wrappers:
getattris replaced withsafe_getattrthat blocks access to:__class__,__bases__,__mro__,__subclasses____globals__,__code__,__builtins__,__dict____self__,__func__,__closure__,__qualname__- And other reflection attributes
hasattris replaced withsafe_hasattrwith the same restrictions
Allowed Builtins:
- Safe functions:
abs,len,max,min,sum,sorted,reversed, etc. - Type constructors:
int,float,str,bool,list,dict,set,tuple - Iteration:
range,enumerate,zip,map,filter - Type checking:
isinstance,type,callable
ProcessSandbox is now the default for run_sandboxed(), running code in a
separate subprocess. This provides the strongest isolation boundary:
- Subprocess isolation: Code executes in a child process with no shared memory. The parent process is unaffected by crashes, hangs, or resource exhaustion in the child.
- Hard timeouts: Uses
process.kill()(SIGKILL) -- no code can prevent termination. Default: 5 seconds, configurable viaProcessSandboxConfig(timeout=...). - Step limits: The interpreter counts each evaluation step. When
max_stepsis set (viaSandboxConfigorRunConfig), execution raisesRUNTIME_STEP_LIMIT(E503) once the budget is exhausted. Step counting covers builtin calls,_values_equal, and_match_patternto prevent budget bypass. The hosted server defaults to 10,000 steps; the embedding API defaults to no step limit unlessRunConfig(max_steps=...)is set. - Memory guards: Configurable via
ProcessSandboxConfig(max_memory_bytes=...). Default: 512 MiB on Darwin and 256 MiB elsewhere. Enforced viaresource.setrlimit(RLIMIT_AS)on Linux/macOS. The runtime also enforces amax_collection_size(default: 10,000,000 elements) and amax_integer_bits(default: ~10,000 digits) to prevent memory exhaustion from within the language itself. - CPU limits: Configurable via
ProcessSandboxConfig(max_cpu_time=...). - File size limits: Default: 0 bytes (no file writing allowed).
- Process limits: Default: 1 (only the main process, no forking).
- Output limits:
max_output_length(default: 100 KB) prevents stdout exhaustion.
from geno.sandbox import run_sandboxed, SandboxConfig
# ProcessSandbox is always used
result, output = run_sandboxed(code)- Sandboxing is enabled by default for both interpreter and compiled code
- Use
--unsafeflag only for trusted code in controlled environments - The sandbox can be configured via
SandboxConfigorProcessSandboxConfig
- Pure computation (arithmetic, logic, data structures)
- Pattern matching and control flow
- User-defined types and functions
- Built-in list/string/option operations
- Print output (to a captured buffer by default)
- Class definitions (via
__build_class__)
- Most
importstatements (only whitelisted modules allowed:dataclasses,typing,math,copy,functools,collections,abc,enum,itertools,typing_extensions) eval()/exec()/compile()- File operations (
open(),read(),write()) - Network operations
- System operations (
os.*,subprocess.*) - Reflection that could bypass sandbox
- Attribute access patterns used for sandbox escapes
- Excessive output (enforced via
max_output_length, default 100KB)
Geno uses capability-based security gating (introduced in 0.2, expanded in 0.3). Every builtin that performs I/O, accesses non-deterministic state, or interacts with the operating system is gated behind a named capability. Programs that do not request a capability cannot call the corresponding builtins.
| Capability | Gated Builtins | Risk Level |
|---|---|---|
print |
print |
Low |
clock |
datetime_now, datetime_format, datetime_parse, datetime_elapsed, clock_now, clock_format, clock_parse, clock_elapsed, sleep_ms |
Low |
random |
math_random_int, math_random_float, random_int, random_float |
Low |
env |
env_get, env_get_or, cli_args |
Medium |
fs |
fs_read_text, fs_write_text, fs_list_dir, fs_exists, fs_metadata, fs_symlink_metadata, fs_canonicalize |
High |
http |
http_fetch, http_post, http_request |
High |
regex |
regex_match, regex_find_all, regex_replace |
Low |
process |
exec, exec_with_input, spawn, spawn_with_input |
Critical |
serve |
http_listen, http_route, http_respond |
High |
stdin |
stdin_read_all |
Medium |
Capability gating is fail-closed:
RunConfig(capabilities=set())— all gated builtins raiseRUNTIME_CAPABILITY_DENIED(E412).RunConfig(capabilities={"print"})— onlyprintis allowed; all others denied.RunConfig()(capabilities omitted /None) — all gated builtins raiseRUNTIME_CAPABILITY_DENIED(E412).
This is the safe default: LLM-generated code cannot perform I/O or access ambient environment, clock, or random builtins unless the host application explicitly opts in. High-risk host callbacks also require the corresponding capability grant.
Capabilities are granted through RunConfig(capabilities={...}). Host applications should
grant the minimum set required:
import geno
# Only grant clock — random, fs, http are all denied
result = geno.run(source, config=geno.RunConfig(capabilities={"clock"}))Important: Host callbacks require explicit capability opt-in. Providing host_callbacks
without a corresponding capabilities set will not install the callbacks:
# SAFE: callbacks are NOT installed because capabilities is not set
config = geno.RunConfig(host_callbacks={"fs_read_text": my_reader})
# CORRECT: explicitly grant the fs capability
config = geno.RunConfig(
capabilities={"fs"},
host_callbacks={"fs_read_text": my_reader},
)High-risk builtins (fs_read_text, http_fetch) are not implemented by Geno. Instead,
the host application provides implementations via RunConfig.host_callbacks. This design
ensures:
- No ambient authority: Geno never touches the filesystem or network on its own.
- Host controls policy: The host callback can enforce path whitelists, rate limits, etc.
- Fail-closed: If a capability is granted but no callback is provided, the builtin raises
RUNTIME_HOST_CALLBACK_MISSING(E413) rather than silently succeeding.
def safe_read(path):
if not path.startswith("/allowed/"):
raise ValueError("path not allowed")
return open(path).read()
config = geno.RunConfig(
capabilities={"fs"},
host_callbacks={"fs_read_text": safe_read},
)- Geno runtime -> Host callback: The runtime passes arguments exactly as the Geno program provides them. The host callback must validate all inputs.
- Host callback -> Geno runtime: The return value is passed back to the Geno program without sanitization. The host callback must ensure the return value is safe.
- Exception propagation: Exceptions raised by host callbacks propagate as Geno runtime errors. The host callback should not leak sensitive information in exception messages.
The import statement resolves modules from an in-memory dictionary, never from the filesystem.
- No filesystem access: Module sources are provided by the host via
RunConfig(modules={...}). The Geno runtime never reads files during module resolution. - No dynamic loading: Module names must be known at parse time (PascalCase identifiers).
- Circular import detection: The typechecker and interpreter both detect and reject circular imports with a clear error message.
- Transitive resolution: If module A imports module B, the host must provide both in the modules dictionary.
modules = {
"Utils": "func double(x: Int) -> Int\n return x * 2\nend func\n",
}
config = geno.RunConfig(modules=modules)
result = geno.run(
"import Utils\nfunc main() -> Int\n return double(2)\nend func\n",
config=config,
)When using Geno to execute LLM-generated code in an agent loop:
- Never grant capabilities blindly. Evaluate each generation step and grant only what is needed for that step.
- Use step budgets. Set
RunConfig(max_steps=...)to limit computation in the embedding API. The hosted server applies a 10,000-step default. - Use timeouts. Set
RunConfig(timeout=...)to enforce wall-clock limits. - Validate before executing. Call
geno.check()first to catch type errors without running the code. - Prefer
modulesover string concatenation. If the agent generates helper functions, pass them as modules rather than concatenating source strings. - Log diagnostics.
RunResult.diagnosticscontains structured error information useful for feeding back to the LLM. - Use Docker for defense in depth. See the reference
Dockerfileanddocker-compose.ymlfor a hardened container configuration with read-only filesystem, memory limits, and unprivileged execution.
The following common Python sandbox escape techniques are blocked:
# BLOCKED: ().__class__.__bases__[0].__subclasses__()
# Static validation catches __class__, __bases__, __subclasses__
# Runtime safe_getattr blocks these if validation is bypassed# BLOCKED: func.__globals__
# Static validation catches __globals__
# safe_getattr blocks access to __globals__# BLOCKED: func.__code__
# Static validation catches __code__
# safe_getattr blocks access to __code__# BLOCKED: func.__builtins__
# safe_getattr blocks access to __builtins__
# compile_and_exec sets up sandbox BEFORE exec()# BLOCKED: getattr(obj, '__class__')
# safe_getattr raises RuntimeError for blocked attributes# BLOCKED: typing.sys.modules['builtins'].open('/etc/hostname')
# Module proxy blocks access to non-allowlisted modules via attribute chains.
# Only modules in the import allowlist can be reached through attributes.
# Additionally, dataclasses.sys, dataclasses.inspect, etc. are all blocked.# BLOCKED: typing.ForwardRef("__import__('os')")._evaluate(...)
# ForwardRef and get_type_hints are blocked at the module proxy level
# because they internally call real eval() on attacker-controlled strings.
# The AST validator also flags private attribute access (_evaluate) as
# defense-in-depth.run_sandboxed()always usesProcessSandbox, which enforces hard timeouts.- The
use_processparameter is deprecated and ignored, and remains only for API compatibility.
- Linux/macOS: Memory limits enforced via
resource.setrlimit()in ProcessSandbox (default) - Windows: Memory limits not enforced (best effort)
When compiling Geno to Python:
- The generated code includes runtime sandbox checks
- Modifying the generated code can bypass security
- Always use
sandboxed=Truewithcompile_and_exec()
The sandbox does not protect against:
- Timing attacks
- Cache-based attacks
- Power analysis
- Other side-channel attacks
Security depends on:
- Python interpreter security
- OS kernel security
- No known vulnerabilities in dependencies
The JS backend (compile_to_js, compile_to_html) targets browser-based execution and has a
different security profile than the Python sandbox.
- Collection size limits: All list, string, set, and array operations enforce a 10,000,000
element cap via
_MAX_COLLECTION_SIZEchecks. Operations that could produce large results (string repeat, list repeat,range, set union,array_new,array_from_list) are guarded. - Regex pattern length limit: Patterns are capped at 1,000 characters (
_MAX_REGEX_PATTERN_LEN) to mitigate ReDoS. - I/O stubs that fail closed:
fs_read_text,fs_write_text,fs_list_dir,fs_exists,fs_metadata,fs_symlink_metadata,fs_canonicalize,exec, andexec_with_inputall fail or returnErrin the browser context rather than silently succeeding. - Immutable constructors: ADT values (
Some,Ok,Err,JsonValue, etc.) are created withObject.freeze, preventing mutation after construction. - HTML escaping:
compile_to_htmlescapes thetitleparameter to prevent injection into the generated HTML document.
- No static code validation: The Python backend runs
validate_code_safety()to scan for dangerous patterns before execution. The JS backend relies on the Geno typechecker and the fact that the compiler only emits code from valid Geno AST nodes -- it does not perform an independent safety scan of the emitted JavaScript. - No import blocking or module proxy: The Python sandbox restricts imports to a small allowlist and wraps modules in a proxy that blocks attribute-chain escapes. The JS output has no equivalent; it runs as a single script with no module system.
- No attribute filtering: The Python sandbox replaces
getattr/hasattrwith safe variants that block reflection attributes (__class__,__globals__, etc.). The JS output does not intercept property access. - No step budget: The Python interpreter supports
RunConfig(max_steps=...)to cap computation. The JS backend has no step counter, so tight infinite loops will run until the browser's own watchdog intervenes. - No process isolation or resource limits: Process-sandboxed Python surfaces use
ProcessSandboxwith hard timeouts, memory limits, and CPU limits viaresource.setrlimit. The JS backend has none of these -- resource governance is delegated to the browser engine. - No Python embedding policy surface: JS output enforces capability gates via
--capin Node.js orglobalThis.__GENO_CAPSin browser hosts, andcompile_to_htmlinjects browser target capabilities at startup. It does not provide Python'sRunConfigor host-callback policy surface; granted capabilities execute against the surrounding JS environment. - No filesystem confinement on Node (
node-clitarget): Python execution confinesfsto configured roots. The JS runtime wires filesystem builtins directly to Node'sfs;--cap fsgrants access to any path the OS user can reach. Geno process builtins are unavailable onnode-cli. The policy-checked Node HTTP bridge useschild_processinternally, but does not expose it as a process capability. The bridge enforces scheme, private-address, redirect, and response-size policies, but these checks do not create an OS sandbox. On the JS target, capability flags select which APIs are wired up; they are not a sandbox. Treat compiled JS artifacts as trusted-code deployments only.
The JS backend is designed for browser-based execution, where the browser engine itself provides a natural sandbox: no filesystem access, no network access beyond fetch (governed by CORS), no process spawning, and built-in memory/CPU limits per tab. This makes the JS backend suitable for:
- Interactive demos and playground environments
- Embedding Geno programs in web pages
- Canvas-based graphical applications via
compile_to_html
It is not suitable for server-side execution (e.g., Node.js) of untrusted code without additional containment.
When serving compile_to_html output or embedding compile_to_js output in a web page:
- Use Content Security Policy headers to restrict the execution environment:
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' - Use iframe sandboxing to isolate Geno output from the host page:
The
<iframe sandbox="allow-scripts" srcdoc="..."></iframe>
sandboxattribute withoutallow-same-originprevents the embedded script from accessing the parent page's DOM, cookies, or storage. - Do not grant
allow-top-navigationin the iframe sandbox, to prevent the embedded script from redirecting the parent page. - Set a
referrerpolicyofno-referreron the iframe to avoid leaking the host URL.
- Never run untrusted code with
--unsafe - Use ProcessSandbox for untrusted code for hard resource limits
- Review code before execution, especially from LLM outputs
- Use appropriate resource limits for your use case
- Keep Geno updated to the latest version
- All PRs are reviewed for security implications
- New built-ins must be evaluated for sandbox safety
- Parser/lexer changes require fuzzing tests
- Runtime changes require security escape tests
- No dependencies with known vulnerabilities
- Security tests must pass before merge
Geno includes comprehensive security tests:
# Run security tests
pytest geno/tests/test_security.py -v
# Run security regression corpus
pytest geno/tests/test_security_corpus.py -v
# Run fuzzing tests (requires hypothesis)
pytest geno/tests/test_fuzzing.py -v
# Run constraint tests
pytest geno/tests/test_constraints.py -v
# Run the maintained security gate (tests, corpus, and Ruff security rules)
make securityNo security advisories have been issued yet.
- Fixed thread sandbox not blocking
__getattribute__/__getattr__dunder definitions in class bodies. The process sandbox already blocked these; the thread sandbox now applies the same AST-based validation unconditionally (#365). - Added XFF header validation in the hosted runtime server.
- Added module size limits to the import resolver.
- Added 1 MB Content-Length cap on hosted server request bodies (returns 400 for oversized requests).
- Fixed module proxy leaking non-allowlisted modules via attribute chains
(e.g.
typing.sys→ rawsys). Non-allowlisted modules are now blocked instead of wrapped. - Fixed
typing.ForwardRef._evaluate()reaching realeval()inside the sandbox.ForwardRefandget_type_hintsare now blocked at the module proxy level. The AST validator also flags private attribute access. - Fixed capability model being fail-open when
RunConfig.capabilitiesis omitted andhost_callbacksare provided. Host callbacks now require explicit capability opt-in. - Fixed
create_handler(allowed_capabilities=set())silently restoring default capabilities due to falsey empty-set check. - Fixed
compile_to_html()injectingtitleinto HTML without escaping. - Added 17 regression tests for all five fixes.
- Eliminated safe-import-allowlist duplication between thread and process sandboxes.
- Added capability-based security gating for all I/O builtins
- Added host callback model for
fs_read_textandhttp_fetch - Added
RUNTIME_CAPABILITY_DENIED(E412) andRUNTIME_HOST_CALLBACK_MISSING(E413) error codes - Added in-memory module resolution (no filesystem access)
- Added circular import detection
- Added security regression corpus with parametrized tests
- Added reference Docker configuration (read-only filesystem, memory limits, unprivileged user)
- Added import fuzzing, capability combo fuzzing, and host callback fuzzing tests
- Added constrained decoding support for
importkeyword
- Added
ProcessSandboxfor hard timeout enforcement - Added
safe_getattrandsafe_hasattrto block reflection attacks - Added static validation enforcement before execution
- Fixed
compile_and_exec()to set up sandbox before execution - Fixed
get_field()to reject underscore-prefixed field names - Added comprehensive security escape tests
- Added property-based fuzzing tests
We thank the following individuals for responsibly disclosing vulnerabilities:
(None yet)