Development guide, built from the current repository. Features may differ from v0.3.0-beta; consult the command reference for the binary documented on this site.

Host process sandboxing

Terra splits each box into a trusted supervisor and confined workers, then confines each worker with the host platform's own mechanism. Every box runs in this built-in sandbox; there is no unconfined launch, and a failed sandbox launch stops the box. The per-platform pages describe each mechanism and draw its process layout:

  • Linux: Bubblewrap namespaces and seccomp filters.
  • macOS: Seatbelt profiles applied by a trusted launcher.
  • Windows: Less-Privileged AppContainers and jobs.

Strictness per platform

What an attacker gets after compromising a worker's native code.

Linux macOS Windows
Mechanism Namespaces + seccomp Seatbelt profile Less-Privileged AppContainer + job
Syscall filter Generated allowlist per role None Win32k calls blocked
Supervisor Seccomp-filtered Unconfined Unconfined
VM worker can…
Reach the network No: own network namespace No No
Read files beyond its grants No Apple's system.sb baseline System files open to all AppContainers
Start another program No: no execve No No: job allows no children
Broker can…
Connect out Any address, host loopback included Any address, host loopback included Any address, through its wrapper
Open a TCP listener No No No
Read files Resolver files and /proc, including the host's /proc/net Resolver files only Its own executable only
Create a VM No: no /dev Yes: shares the hypervisor entitlement No: no hypervisor capability
Memory cap None None 512 MiB commit

Linux is the strictest: every role, the supervisor included, runs under a syscall allowlist. The supervisor never handles guest RAM or packets, so leaving it unconfined on macOS and Windows matters only if a worker escapes first. Windows adds a job and exploit mitigations but filters no syscalls. macOS rests on a single Seatbelt profile per worker, built on a private, deprecated Apple interface. On every platform a compromised broker keeps outbound network access, so use a host firewall when that matters. Native validation on macOS and Windows is still pending; see todo.md.

Processes

Every platform runs the same processes; only the confinement around the two workers differs.

terra (CLI)            your user; exits once a detached box is ready
└─ supervisor          box lifetime, run lock, published listeners
   ├─ VM worker        sandboxed; guest RAM, disks, shares, devices
   └─ network broker   sandboxed; host sockets, resolution, egress

VM worker ◄── IPC ──► network broker   bounded requests, no sockets
terra (CLI) ── agent and control sockets ──► VM worker
Role Holds Never receives
Supervisor Box lock, configuration, worker lifetime, trusted PID files, published listener binding Guest RAM or network packet payloads
VM worker Guest RAM, disks, shares, the hypervisor, Wasm components, broker IPC Host network sockets
Network broker External TCP/UDP sockets, resolution, handed-over published listeners, native policy engine VM runtime handles or RAM through Terra's IPC, or authority to listen

Network broker

Socket descriptors never cross the VM-to-broker channel: the VM receives bounded data and opaque resource handles, and the broker authorizes every operation even if the VM's frontend is compromised. A compromised broker still holds whatever network access its OS sandbox allows; see network policy limits. Local-only boxes (network.enabled: false) run no broker.

The supervisor binds every published listener on host loopback before it starts the broker or installs its own sandbox, and passes them on the broker's startup channel (Unix descriptor passing, Windows socket duplication). The broker checks that each matches its grant and only accepts on them; it never binds a listener itself.

Policies

Each role's policy is built into the executable; no setting, file or environment variable changes it at run time. On Linux the policy includes syscall allowlists generated from the code; see host sandbox policy.

No platform sandbox supplies hard CPU, memory, disk or bandwidth quotas, except the Windows broker's commit limit.

Linux sandboxing

The supervisor starts the VM worker and, with networking enabled, the network broker under Bubblewrap, which ships inside the terra executable. Every role also runs under seccomp filters; host process sandboxing covers what each role holds.

Process layout

terra (CLI)                 your user, unconfined
└─ terra __supervisor       role filter + allowlist once the workers
   │                        start; holds the run lock and listeners
   ├─ bwrap ─► __vm         own user, PID, mount, IPC, UTS and
   │                        network namespaces; role filter and its
   │                        allowlist; /dev/kvm, box state, shares
   └─ bwrap ─► __network    own user, PID, mount, IPC and UTS
                            namespaces; host network; role filter
                            and its allowlist; no /dev, no /tmp

__vm ◄── socket pair (fd 7) ──► __network
__supervisor ── startup channel (fd 8) ──► __network
__supervisor ── run lock (fd 3) ──► __vm
terra (CLI) ── agent and control sockets ──► __vm

Namespaces and capabilities

  • Both workers get separate mount, user, PID, IPC and UTS namespaces and drop all capabilities. The VM also gets a private network namespace, so its native sockets cannot reach host networking; the broker keeps the host network namespace.
  • The broker gets no /dev device nodes and no /tmp. It does get /proc, where /proc/net lists the host's connections and Unix socket names.
  • The launcher clears the inherited environment and forwards only diagnostic and worker metadata. Guest environment grants travel in the boot plan; broker policy and listener grants arrive on a supervisor-controlled startup channel that VM requests cannot alter.

Seccomp filters

  • Bubblewrap installs each worker's role filter after namespace setup and before exec; the worker installs its compiled-in syscall allowlist as it starts. The supervisor installs its own lifecycle filter and allowlist. Allowlists are generated by statically scanning each role's code (host sandbox policy).
  • Workers stay in the host session, so the role filters and allowlists deny the TIOCSTI and TIOCLINUX terminal-injection ioctls.
  • The broker shares the host network namespace, where abstract Unix sockets (such as an X11 display) and host netlink live. Its filters allow only IPv4 and IPv6 sockets and Unix stream socket pairs, and deny listen; the supervisor reads host interface addresses for it.

Lifetime

  • host.pid names the VM in the host PID namespace and supervisor.pid the process owning box lifetime. Workers cannot rewrite these through their filesystem grants. Stop and failed startup clean up the whole process tree.
  • The VM holds a duplicate run-lock descriptor, so supervisor death cannot let another boot reuse the disks before the old VM exits. The lock is cooperative: compromised native VM code can unlock its own box.
  • Broker failure after startup leaves the VM and agent running with networking unavailable; there is no reconnect or fallback to direct networking.

Validation

The built-in launcher targets the statically linked musl release executable. Native KVM and negative enforcement tests are required per architecture.

macOS sandboxing

Each worker runs under a Seatbelt profile that a trusted launcher applies before the worker's code runs; the supervisor stays unconfined. Host process sandboxing covers what each role holds.

Process layout

terra (CLI)                 your user, unconfined
└─ terra __supervisor       unconfined; prebinds the agent, stop and
   │                        published listeners
   ├─ __sandbox_launcher    trusted; applies the VM profile, then execs:
   │  └→ __vm               Hypervisor.framework, granted files and
   │                        shares; no network, no child processes
   └─ __sandbox_launcher    trusted; applies the broker profile, then execs:
      └→ __network          network connections, resolver files; no
                            disks, shares or child processes

└→ is an exec: the launcher becomes the worker and keeps its PID and descriptors.

__vm ◄── socket pair ──► __network
__supervisor ── startup channel ──► __network
__supervisor ── agent and stop listeners, run lock ──► __vm
terra (CLI) ── agent and control sockets ──► __vm

Profiles

Every profile denies by default, imports Apple's system.sb baseline and is built into terra. A role adds these rules to the shared base, which also lets the worker exec its own executable.

Worker Allowed on top of the base
VM Its granted files and shares, and the FSEvents daemon for share notifications; no network, no child processes
Broker Name resolution through mDNSResponder, outbound connections to any IP (including host loopback), UDP binds and inbound UDP, socket creation, granted resolver files; no TCP binds, so it cannot open TCP listeners; no disks, shares or child processes

The broker accepts on listeners the supervisor binds and hands over, which needs no network rule.

Launcher

The supervisor writes an owner-only spec (role, canonical grants) under ~/.terra. The launcher renders the profile with the executable and every grant passed as parameters, so no path from the spec enters the profile text. It applies the profile with sandbox_init_with_parameters and execs the worker. There is no per-launch signing. The shipped executable is signed once with Hardened Runtime and packaging/macos.entitlements, so both workers carry the hypervisor and unsigned-executable-memory entitlements. Hypervisor.framework is not gated by Seatbelt, so no profile keeps the broker from creating a VM.

Activation check

Before initializing, each worker must fail to open a private canary file (the launch spec) outside its grants, and the VM must fail IPv4 and IPv6 loopback TCP connects and listens. Any other outcome fails startup and reaps the worker; a successful profile call alone is not trusted.

Files

Grants are profile rules on canonical paths: Seatbelt matches resolved paths, so /tmp grants become /private/tmp. Each grant's ancestor directories also get a metadata-only rule, because the file-events watcher canonicalizes each share path. The VM can also send IPC to the FSEvents daemon, which that watcher subscribes through; the daemon delivers only events for paths the VM's file rules allow. A read-only grant inside a writable grant is still rejected; no caller needs it. The VM gets the box directory read-only plus specific writable disk, diagnostics and runtime-logs/ paths. File rules do not grant Unix-socket authority, so the supervisor prebinds the agent and stop listeners and passes them to the VM as descriptors.

Limits

  • Both profiles include Apple's system.sb baseline. There are no namespaces, seccomp filters or mount remapping.
  • The broker keeps outbound network access, including host loopback, and can reach mDNSResponder. It cannot bind TCP sockets, so it cannot open TCP listeners; UDP binds work.
  • Both workers are the same signed binary and carry the hypervisor entitlement, and Seatbelt does not gate Hypervisor.framework, so a compromised broker could create a VM.
  • Seatbelt's profile interface is private and deprecated, so a macOS update could change it.
  • Boxes start over SSH: Seatbelt needs no GUI login session.

Validation

Native acceptance on Apple Silicon macOS 15+, from an SSH session:

make TERRA_TARGET=aarch64-apple-darwin host-dist
codesign --force --sign - --options runtime --entitlements packaging/macos.entitlements dist/terra
TERRA_BIN=$PWD/dist/terra CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER="sh $PWD/scripts/toolchain/run-macos-vm-test" \
  cargo test -p terra-sandbox --lib macos -- --ignored --test-threads=1 --nocapture

Then run the packaged VM tests and exercise guest TCP, UDP, DNS, published listeners, shares, stop, detached lifetime and abrupt supervisor or broker death.

Windows sandboxing

Each worker runs in its own Less-Privileged AppContainer, created per launch with a unique profile and package SID. A trusted wrapper launches it suspended inside a single-process job and checks the result before the worker starts. Host process sandboxing covers what each role holds.

Process layout

terra.exe (CLI)             your user, unconfined
└─ terra.exe __supervisor   unconfined; binds published listeners
   ├─ __sandbox_launcher    trusted wrapper: DACL grants, console
   │  │                     relay, guest symlinks
   │  └─ __vm               AppContainer and job; HypervisorPlatform,
   │                        registryRead; granted box files, shares
   └─ __sandbox_launcher    trusted wrapper: opens TCP and UDP
      │                     sockets for the broker
      └─ __network          AppContainer and job, 512-MiB commit
                            limit; internetClient, registryRead

__vm ◄── socket pair ──► __network
__supervisor ── startup channel ──► __network
__network ── socket requests ──► wrapper ── sockets ──► __network
__vm ── symlink requests ──► wrapper
terra.exe (CLI) ── agent and control sockets ──► __vm

Capabilities

Worker Capabilities Files Networking Other limits
VM HypervisorPlatform, registryRead Approved grants None Dynamic code allowed for Wasmtime's AOT components
Broker internetClient, registryRead Its executable only Sockets opened by its wrapper, name resolution 512-MiB commit limit, dynamic code disabled

The role policies are built in and cannot be overridden.

Both workers

The job kills the worker when the last job handle closes, permits no child processes, and blocks desktop, clipboard, global atom and USER-handle access. Workers get no Win32k system calls, forced and high-entropy ASLR, permanent strict handle checks, heap termination, module tampering protection, branch-target isolation and speculative store bypass disable. They load no legacy extension DLLs, non-system fonts, or images from remote shares or with a low integrity label, and resolve DLLs from System32 first. Workers are never attached to a console: console input becomes NUL, and console output passes through a wrapper relay as text with control characters replaced, so a worker cannot queue keystrokes for the user's shell or send terminal escape sequences. Before running, each worker must find its AppContainer token with exactly its role's capabilities, its job, and no reachable console; any other result fails startup.

VM grants

The VM's package SID is the only one added to approved file DACLs; orderly exit removes the grants and deletes the profile. Read-only grants carry explicit write/delete/permission denies; writable directories omit FILE_DELETE_CHILD. The VM gets no grant on the box directory itself, only on the files its box configuration names: the root disk, configured volumes and logs read-write, and box metadata read-only. Grant traversal skips reparse points, and share roots cannot be reparse points. Filesystem operations resolve guest paths within the approved share; a launch may grant at most 16,384 objects.

WHP

The VM's HypervisorPlatform capability permits access to Windows Hypervisor Platform; registryRead lets Winsock initialize for inherited IPC. The VM receives no network capabilities. A WHP failure aborts the launch without retrying.

Broker sockets

AppContainer network isolation blocks a container's own sockets from host loopback, which published listeners and HOST_LOOPBACK rules need, and the broker's token lacks the capability to accept connections on its own sockets. Its published listeners come from the supervisor. For outbound traffic the broker asks its wrapper, over an inherited channel, to connect a TCP socket to a peer its policy approved or for a UDP socket bound to an ephemeral port; the wrapper completes the connection, within 30 seconds and at most 64 at a time, and duplicates the socket into the broker. No socket the broker can obtain is able to listen. The broker has no file grants beyond its executable, so it cannot read user files, box state or the user's other processes, and its commit limit bounds memory.

The broker accepts on the listeners the supervisor bound and opens IPv4 or IPv6 TCP/UDP sockets through its wrapper. It applies destination policy before requesting them; published listeners claim their ports with SO_EXCLUSIVEADDRUSE, so no other local process can bind the same port. The VM receives no native sockets and uses the bounded broker IPC protocol for network operations.

Lifetime

The wrapper watches the supervisor process and stops the worker if the supervisor exits. Only standard I/O, the IPC sockets, the broker's socket channel, the VM's symlink channel, approved listeners and run-lock handle are inherited. If the wrapper itself is killed, the job still ends the worker, but DACL and profile cleanup cannot run; profile names are never reused.

Guest symlinks in host-shared directories are stored the way WSL stores them (IO_REPARSE_TAG_LX_SYMLINK). AppContainer can neither set nor read any reparse point, so a VM with mounts passes link requests to its wrapper over an inherited channel. Each request names a share the supervisor validated and a path beneath it: the wrapper creates links only beneath writable shares, reads them beneath any of the VM's shares, and does nothing else. Windows never follows these links, so a guest-planted link cannot redirect host programs; Windows tools other than WSL see them as files they cannot open. The wrapper refuses absolute targets, as other hosts do. Windows symlinks already in a share still read as symlinks.

Limits

No mount, PID or user namespaces and no seccomp. A compromised broker can request any number of wrapper connections and UDP sockets, so it keeps outbound host network access, including host loopback, and can receive on its UDP sockets.

Validation

The compiled probe runs both workers under these policies, reads each worker's mitigations back from the kernel, and checks VM grants, WHP access, symlinks through the wrapper and denied VM networking:

$env:TERRA_BIN = (Resolve-Path dist\terra.exe).Path
cargo test -p terra-sandbox --lib windows::tests::native_roles_enforce_vm_grants_and_broker_job -- --ignored

Native validation must also show that VM TCP/UDP fails for loopback, private and public destinations while broker IPC works; that host AF_UNIX paths in shares give no tunnel; that the VM cannot replace the PID files; and that killing the supervisor or wrapper at any phase leaves no worker or live socket.