Gengoscript Security Model

This page describes the security boundary Gengoscript is designed to provide and the controls a host should use when running untrusted scripts.

Threat Model

Gengoscript assumes:

Gengoscript is a language-level isolation boundary, not an operating-system sandbox. A script runs inside the Gengoscript VM, not in a separate process. For higher-risk deployments, run gengo-engine.wasm inside a WebAssembly runtime as an additional isolation layer.

What Scripts Cannot Do by Default

Without explicit host opt-in, a script cannot:

std is the only built-in import. It provides language utilities and does not grant ambient access to the machine.

Instruction Budget

Each VM opcode decrements a counter. When the counter reaches zero, execution stops with error.InstructionBudgetExceeded.

var rt = api.Runtime.init(.{
    .allow_io = false,
    .max_ops = 100_000,
});

Set max_ops in production. null means unlimited execution and should normally be treated as a development setting.

Resource Limits

In addition to the instruction budget, the engine enforces hard limits on memory and call depth.

FieldWhat it limits
heap_size_bytesTotal Gengoscript heap
max_objectsLive GC object count
max_stackVM value stack depth
max_framesCall frame depth
max_defersDeferred call depth

These limits come from the active build preset and may be tightened per instance.

PresetHeapIntended use
256k256 KiBConstrained embedded targets
1m1 MiBDefault — CLI and general scripting
16m16 MiBProduction embedding / large workloads
unlimited256 MiBNo practical limits

The heap allocator's largest block size scales with the configured heap (capped at heap/8, floor 64 KiB), so the 16m preset lifts the single-allocation ceiling to 2 MiB.

Capability Modules

System access is opt-in through capability modules:

CapabilityImport pathPurpose
httpcap:httpOutbound HTTP
fscap:fsFilesystem access through named mounts
netcap:netRaw network operations
envcap:envRead-only process environment access

Enabling one capability does not enable the others.

The full target and host-registration inventory is capability-matrix.md. In particular, cap:env can expose inherited secrets: enable it only with a host-side allowlist. cap:http and cap:net callbacks are host authority; use default-deny address, port, redirect, timeout, and response-size policies for untrusted scripts. The VM operation budget does not account for time spent inside host callbacks.

For cap:fs, scripts can only reach host-registered mounts. Absolute paths and path traversal are rejected before any syscall.

Network and HTTP policy

Do not enable cap:http or cap:net for untrusted scripts with an implicit allow-all policy. In particular, the current native network dial policy allows all destinations when it has no rules. The host must provide the restriction; the VM cannot infer a safe destination from a URL or address string.

net's listen scope (inbound sockets, via net.listen/Listener.accept) is a materially bigger authority than dial and is gated deliberately differently on two axes:

has always meant dial-only and continues to on upgrade — it never silently starts granting listen. A host must explicitly opt in with --cap net=listen or --cap net=dial,listen.

listen (bind) policy refuses everything until the host adds at least one explicit allow rule via engine_net_listen_policy_add. This is a deliberately different default from dial's: a bad dial destination is one outbound request the script chose; an open listening port that mishandles input is remotely reachable, by anyone, indefinitely, regardless of what the script does. The dial and listen policy rule lists are entirely separate — configuring one has no effect on the other.

A host allowing listen should also consider that listening sockets are long-lived by nature: two independent Runtimes sharing one embedding process currently share one connection/listener table and one set of policy rules for each of dial and listen (the same latent cross-runtime state sharing dial already has — see the multi-runtime isolation tracking issue). For a single-Runtime-per-process embedding (the CLI, and most host integrations) this is not a concern; a host running multiple independently untrusted scripts with listen enabled in one process should treat that sharing as a real isolation gap until it's resolved, not assume it's already handled.

A restrictive policy should, at minimum:

and ports;

and reject loopback, link-local, private, and other internal ranges unless explicitly required;

rebinding by validating the address used for each connection;

response sizes; and limit redirect count; and

response is trustworthy.

These controls defend against SSRF and data exfiltration. They also prevent a small number of VM instructions from causing a long blocking host operation. The instruction budget does not interrupt a running HTTP or socket callback.

Filesystem policy

Mount names restrict the script-visible path syntax, but they are not a complete filesystem sandbox. The host or filesystem driver remains responsible for symlink escape, time-of-check/time-of-use races, device files, recursive enumeration, quotas, and per-file or total-size limits. Prefer a dedicated, read-only directory or a virtual driver for untrusted read-only workloads. Do not mount a writable application, configuration, or credential directory merely because path traversal is rejected.

Environment policy

cap:env is read-only but can disclose credentials, tokens, proxy settings, and deployment metadata inherited by the host process. Prefer passing one specific non-secret value through env.get, or better, through a narrowly designed host: function. Do not expose env.list() to untrusted scripts when the process environment contains secrets. WASI and native hosts can have different inherited environments; review them independently.

Import Sandboxing

When the CLI runs a script, file imports are restricted to the script's own directory. Any import that would resolve outside that directory is rejected at compile time with ImportOutsideRoot:

gengo: compile error: ImportOutsideRoot: import '../shared/utils' is outside the allowed source directories

Additional directories can be whitelisted with --modules (repeatable):

gengo --modules /app/lib script.gengo

Embedded runtimes created through the Zig API are unrestricted unless source_root is configured explicitly in the Config. .table and .callback source providers bypass filesystem resolution entirely and are unaffected by this restriction.

Host Modules

Host-defined modules are imported through host: paths such as import("host:db"). Scripts can call only the functions the host explicitly registers.

Host functions run in ordinary host code, outside the VM instruction budget. They should therefore be treated as trusted integration points and kept fast and predictable.

Host callbacks should enforce their own input-size, timeout, allocation, and concurrency limits. They must not assume that a bounded VM operation count means a bounded amount of host work. Treat callback re-entry into the same engine as an advanced integration case and follow the ownership and reentry rules in host-abi.md; independent engine instances are not an operating-system isolation boundary against memory-unsafe native code or a malicious callback.

Instance Isolation

Each runtime instance has its own heap, globals, stack, and call frames. Errors, panics, or budget exhaustion in one instance do not affect another instance.

Output Control

Set allow_io = false unless scripts should be able to write through std.io:

var rt = api.Runtime.init(.{ .allow_io = false });

This suppresses built-in script output only. It does not prevent host callbacks from performing I/O.

Confusable Identifiers

Unicode identifiers are not normalized. Two identifiers that look identical but are composed differently (for example, NFC vs NFD) are treated as distinct names. This creates the same confusable-identifier risk as Go. Review scripts that accept untrusted source code, and consider running them through a normalizing preprocessor if visual spoofing is a concern.

Deployment Checklist

For production use: