Skip to content

Security: davidiach/geno-lang

SECURITY.md

Security Policy

Supported Versions

Version Supported
0.5.x ✅
0.4.x ✅
0.3.x ❌
0.2.x ❌
0.1.x ❌

Reporting a Vulnerability

If you discover a security vulnerability in Geno, please report it responsibly:

  1. Do NOT open a public GitHub issue for security vulnerabilities.
  2. Use GitHub private vulnerability reporting.
  3. 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)

Threat Model

Execution Surfaces

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.

Intended Use Case

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

Out of Scope

  • 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

Security Architecture

Multi-Layer Defense

Geno implements defense in depth with three layers:

  1. Static Validation - Code is analyzed before execution to detect dangerous patterns
  2. Runtime Sandbox - Execution occurs with restricted builtins and safe wrappers
  3. Process Isolation - Used by process-sandboxed surfaces for hard resource limits

Layer 1: Static Code Validation

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.

Layer 2: Runtime Sandbox

The sandbox provides:

Blocked Builtins:

  • eval, exec, compile - Code execution
  • open - File system access
  • input - User input (can hang)
  • __import__ - Module imports
  • globals, locals, vars - Namespace introspection
  • breakpoint, help, license - Interactive functions
  • memoryview - Low-level memory access
  • setattr, delattr - Object mutation

Safe Wrappers:

  • getattr is replaced with safe_getattr that blocks access to:
    • __class__, __bases__, __mro__, __subclasses__
    • __globals__, __code__, __builtins__, __dict__
    • __self__, __func__, __closure__, __qualname__
    • And other reflection attributes
  • hasattr is replaced with safe_hasattr with 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

Layer 3: Process Isolation (ProcessSandbox) - DEFAULT

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 via ProcessSandboxConfig(timeout=...).
  • Step limits: The interpreter counts each evaluation step. When max_steps is set (via SandboxConfig or RunConfig), execution raises RUNTIME_STEP_LIMIT (E503) once the budget is exhausted. Step counting covers builtin calls, _values_equal, and _match_pattern to prevent budget bypass. The hosted server defaults to 10,000 steps; the embedding API defaults to no step limit unless RunConfig(max_steps=...) is set.
  • Memory guards: Configurable via ProcessSandboxConfig(max_memory_bytes=...). Default: 512 MiB on Darwin and 256 MiB elsewhere. Enforced via resource.setrlimit(RLIMIT_AS) on Linux/macOS. The runtime also enforces a max_collection_size (default: 10,000,000 elements) and a max_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)

Sandboxed Execution

Safe by Default

  • Sandboxing is enabled by default for both interpreter and compiled code
  • Use --unsafe flag only for trusted code in controlled environments
  • The sandbox can be configured via SandboxConfig or ProcessSandboxConfig

What's Allowed

  • 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__)

What's Blocked

  • Most import statements (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)

Capability Threat Model

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 Map

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

Denied by Default

Capability gating is fail-closed:

  • RunConfig(capabilities=set()) — all gated builtins raise RUNTIME_CAPABILITY_DENIED (E412).
  • RunConfig(capabilities={"print"}) — only print is allowed; all others denied.
  • RunConfig() (capabilities omitted / None) — all gated builtins raise RUNTIME_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.

Granting Capabilities

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},
)

Host Callback Trust Model

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:

  1. No ambient authority: Geno never touches the filesystem or network on its own.
  2. Host controls policy: The host callback can enforce path whitelists, rate limits, etc.
  3. 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},
)

Trust Boundaries

  • 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.

Module Security

The import statement resolves modules from an in-memory dictionary, never from the filesystem.

Design Principles

  1. No filesystem access: Module sources are provided by the host via RunConfig(modules={...}). The Geno runtime never reads files during module resolution.
  2. No dynamic loading: Module names must be known at parse time (PascalCase identifiers).
  3. Circular import detection: The typechecker and interpreter both detect and reject circular imports with a clear error message.
  4. Transitive resolution: If module A imports module B, the host must provide both in the modules dictionary.

Example

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,
)

Agent Execution Guidance

When using Geno to execute LLM-generated code in an agent loop:

  1. Never grant capabilities blindly. Evaluate each generation step and grant only what is needed for that step.
  2. Use step budgets. Set RunConfig(max_steps=...) to limit computation in the embedding API. The hosted server applies a 10,000-step default.
  3. Use timeouts. Set RunConfig(timeout=...) to enforce wall-clock limits.
  4. Validate before executing. Call geno.check() first to catch type errors without running the code.
  5. Prefer modules over string concatenation. If the agent generates helper functions, pass them as modules rather than concatenating source strings.
  6. Log diagnostics. RunResult.diagnostics contains structured error information useful for feeding back to the LLM.
  7. Use Docker for defense in depth. See the reference Dockerfile and docker-compose.yml for a hardened container configuration with read-only filesystem, memory limits, and unprivileged execution.

Known Escape Vectors (Mitigated)

The following common Python sandbox escape techniques are blocked:

1. Class Hierarchy Escape

# BLOCKED: ().__class__.__bases__[0].__subclasses__()
# Static validation catches __class__, __bases__, __subclasses__
# Runtime safe_getattr blocks these if validation is bypassed

2. Globals Access

# BLOCKED: func.__globals__
# Static validation catches __globals__
# safe_getattr blocks access to __globals__

3. Code Object Manipulation

# BLOCKED: func.__code__
# Static validation catches __code__
# safe_getattr blocks access to __code__

4. Builtins Override

# BLOCKED: func.__builtins__
# safe_getattr blocks access to __builtins__
# compile_and_exec sets up sandbox BEFORE exec()

5. Dynamic Attribute Access

# BLOCKED: getattr(obj, '__class__')
# safe_getattr raises RuntimeError for blocked attributes

6. Module Attribute Chain Escape

# 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.

7. typing.ForwardRef eval() Escape

# 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.

Known Limitations

1. Compatibility Parameters

  • run_sandboxed() always uses ProcessSandbox, which enforces hard timeouts.
  • The use_process parameter is deprecated and ignored, and remains only for API compatibility.

2. Memory Limits

  • Linux/macOS: Memory limits enforced via resource.setrlimit() in ProcessSandbox (default)
  • Windows: Memory limits not enforced (best effort)

3. Compiled Code

When compiling Geno to Python:

  • The generated code includes runtime sandbox checks
  • Modifying the generated code can bypass security
  • Always use sandboxed=True with compile_and_exec()

4. Side Channels

The sandbox does not protect against:

  • Timing attacks
  • Cache-based attacks
  • Power analysis
  • Other side-channel attacks

5. Python Vulnerabilities

Security depends on:

  • Python interpreter security
  • OS kernel security
  • No known vulnerabilities in dependencies

JavaScript Backend Security

The JS backend (compile_to_js, compile_to_html) targets browser-based execution and has a different security profile than the Python sandbox.

What the JS Backend Provides

  • Collection size limits: All list, string, set, and array operations enforce a 10,000,000 element cap via _MAX_COLLECTION_SIZE checks. 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, and exec_with_input all fail or return Err in the browser context rather than silently succeeding.
  • Immutable constructors: ADT values (Some, Ok, Err, JsonValue, etc.) are created with Object.freeze, preventing mutation after construction.
  • HTML escaping: compile_to_html escapes the title parameter to prevent injection into the generated HTML document.

What the JS Backend Does NOT Provide (vs. Python Sandbox)

  • 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/hasattr with 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 ProcessSandbox with hard timeouts, memory limits, and CPU limits via resource.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 --cap in Node.js or globalThis.__GENO_CAPS in browser hosts, and compile_to_html injects browser target capabilities at startup. It does not provide Python's RunConfig or host-callback policy surface; granted capabilities execute against the surrounding JS environment.
  • No filesystem confinement on Node (node-cli target): Python execution confines fs to configured roots. The JS runtime wires filesystem builtins directly to Node's fs; --cap fs grants access to any path the OS user can reach. Geno process builtins are unavailable on node-cli. The policy-checked Node HTTP bridge uses child_process internally, 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.

Intended Use Case

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.

Recommended Deployment Hardening

When serving compile_to_html output or embedding compile_to_js output in a web page:

  1. Use Content Security Policy headers to restrict the execution environment:
    Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'
    
  2. Use iframe sandboxing to isolate Geno output from the host page:
    <iframe sandbox="allow-scripts" srcdoc="..."></iframe>
    The sandbox attribute without allow-same-origin prevents the embedded script from accessing the parent page's DOM, cookies, or storage.
  3. Do not grant allow-top-navigation in the iframe sandbox, to prevent the embedded script from redirecting the parent page.
  4. Set a referrerpolicy of no-referrer on the iframe to avoid leaking the host URL.

Security Best Practices

For Users

  1. Never run untrusted code with --unsafe
  2. Use ProcessSandbox for untrusted code for hard resource limits
  3. Review code before execution, especially from LLM outputs
  4. Use appropriate resource limits for your use case
  5. Keep Geno updated to the latest version

For Contributors

  1. All PRs are reviewed for security implications
  2. New built-ins must be evaluated for sandbox safety
  3. Parser/lexer changes require fuzzing tests
  4. Runtime changes require security escape tests
  5. No dependencies with known vulnerabilities
  6. Security tests must pass before merge

Security Testing

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 security

Security Advisories

No security advisories have been issued yet.

Changelog

v0.3.x Security Fixes

  • 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).

v0.2.1 Security Fixes

  • Fixed module proxy leaking non-allowlisted modules via attribute chains (e.g. typing.sys → raw sys). Non-allowlisted modules are now blocked instead of wrapped.
  • Fixed typing.ForwardRef._evaluate() reaching real eval() inside the sandbox. ForwardRef and get_type_hints are now blocked at the module proxy level. The AST validator also flags private attribute access.
  • Fixed capability model being fail-open when RunConfig.capabilities is omitted and host_callbacks are 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() injecting title into HTML without escaping.
  • Added 17 regression tests for all five fixes.
  • Eliminated safe-import-allowlist duplication between thread and process sandboxes.

v0.2.0 Security Improvements

  • Added capability-based security gating for all I/O builtins
  • Added host callback model for fs_read_text and http_fetch
  • Added RUNTIME_CAPABILITY_DENIED (E412) and RUNTIME_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 import keyword

v0.1.x Security Improvements

  • Added ProcessSandbox for hard timeout enforcement
  • Added safe_getattr and safe_hasattr to 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

Acknowledgments

We thank the following individuals for responsibly disclosing vulnerabilities:

(None yet)

There aren't any published security advisories