isobox

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 18 Imported by: 0

README

isobox

Run a command in a sandbox, with the same flags on every OS.

CI Go Reference Go Report Card License Go

Simple universal sandboxing with preload capabilities.
One policy compiles to Seatbelt, gVisor, or AppContainer, and your command's exit code passes straight through.


isobox -- echo hi
OS Backend Mechanism
macOS Seatbelt sandbox-exec
Linux gVisor runsc
Windows AppContainer in-process Win32

The backend is chosen from GOOS and can be overridden with --backend.

The problem it solves

Every OS sandbox has its own vocabulary and its own gaps. A profile that denies network on macOS does not transfer to Linux, and the two do not even agree on what "deny network" includes. isobox papers over this with an explicit capability model instead of a lowest-common-denominator illusion:

  • Each backend declares the set of capabilities it can actually enforce.
  • Build your policy from the intersection of all backends and the sandbox behaves identically everywhere.
  • Reach for one backend's union extras and isobox still runs, but attaches a queryable caveat wherever enforcement falls short of the stated intent.
  • --strict refuses anything outside the portable intersection, so you fail loudly instead of silently degrading.

Because the capability tables are plain data, you can inspect or preview any backend's plan from any host. Running it still needs that backend's tool installed.

Install

Install Go 1.26+ and just, then:

just build
just test

Without just:

go build -o isobox ./cmd/isobox
go test ./...

On Linux, the optional LD_PRELOAD filesystem fallback is C:

just isoboxfs-build
just isoboxfs-test

This builds preload/isoboxfs/libisoboxfs.so from the C sources under preload/isoboxfs.

GitHub Actions runs just build and just test on Ubuntu, macOS, and Windows, plus the C preload build and test on Ubuntu.

Quick start

isobox -- echo hi                              # agent defaults (see Profiles)
isobox --profile=tight -- echo hi              # locked down: no net, read-only, no writes
isobox --net=enable -- curl https://example.com
isobox --writable ./work -- just build         # persist ./work plus the default cwd
isobox --write=ephemeral -- ./build.sh         # writes happen, then vanish
isobox --cpus 1.5 --memory 512m -- ./build.sh  # cap CPU and memory
isobox --print --backend gvisor -- ls /        # preview the Linux plan from any OS
isobox --caps                                  # print the capability matrix

Everything after -- is the command and its arguments.

Profiles

A profile sets the defaults; explicit flags always override it.

  • agent (default): outbound-only network, the command working directory and OS temp writable, broad host reads with common credential paths denied, and writes elsewhere kept off the host. On gVisor this is a memory-backed root overlay plus persistent writable binds. Seatbelt and AppContainer cannot redirect outside writes, so they deny them and report the caveat in the plan.
  • tight: no external network, broad reads, no writes anywhere.

Capabilities

isobox --caps prints the live matrix (yes = enforced, - = unsupported):

CAPABILITY          WINDOWS  DOCKER-EPHEMERAL  GVISOR  SEATBELT  PORTABLE  DESCRIPTION
fs.read.deny        -        -                 yes     yes       -         read broadly except denied sensitive paths
fs.read.host        -        -                 yes     yes       -         read the host filesystem broadly
fs.read.scope       yes      -                 yes     yes       -         restrict reads to an allowlist
fs.write.deny       -        -                 yes     yes       -         deny all writes to the host filesystem
fs.write.ephemeral  -        -                 yes     yes       -         permit writes but discard them; host untouched
fs.write.scope      yes      -                 yes     yes       -         permit writes only to listed paths, persisted to host
ipc.restrict        -        -                 yes     -         -         no host local IPC endpoint reachable
kernel.isolation    -        -                 yes     -         -         serve syscalls from a user-space kernel; shield host kernel
mach.restrict       -        -                 -       yes       -         restrict Mach service lookups (Seatbelt-only)
net.disable         yes      yes               yes     yes       yes       deny network access; some backends additionally block loopback (see caveats)
net.enable          yes      yes               yes     yes       yes       permit network access
net.outbound        -        -                 yes     yes       -         permit outbound connections, block inbound/listen
proc.no_exec        yes      -                 yes     -         -         forbid executing another program image
res.cpu             yes      yes               yes     -         -         limit CPU usage to a fraction of the host's cores
res.memory          yes      yes               yes     -         -         limit the sandbox's memory footprint

PORTABLE marks the intersection: capabilities every backend enforces, so a spec built only from them runs identically everywhere.

Inspecting a plan

--print compiles a spec to a backend's plan and shows it without running. It works for any backend on any host, so you can review the exact Linux invocation from a Mac:

$ isobox --print --backend gvisor -- just build
backend:  gvisor
enforces: fs.read.deny, fs.read.host, fs.write.ephemeral, fs.write.scope, kernel.isolation, net.outbound
filesystem: linux-namespace-view
caveats:
  - host filesystem scopes can expose host IPC endpoints
  - gvisor overlay flag syntax varies by runsc version (used --overlay2=root:memory)
  ...
argv:
  runsc --overlay2=root:memory --oci-seccomp run --bundle <bundle> isobox-gvisor-...

For Seatbelt the plan also includes the generated SBPL profile; for AppContainer it includes the resolved grants.

CLI reference

Flag Default Meaning
--profile=agent|tight agent Apply defaults. tight restores the no-network/no-write posture.
--net=disable|enable|outbound profile Network policy. outbound allows clients but blocks listen/inbound.
--write=none|scope|ephemeral|overlay profile Deny writes, persist only --writable, discard all writes, or persist writable paths with shadow/deny elsewhere.
--writable PATH cwd in profile Repeatable. Writable path; adds to the agent cwd grant, or implies --write=scope under tight.
--readable PATH Repeatable. Restrict reads to these paths where the backend supports it.
--read-deny PATH credentials Repeatable. Deny reads to sensitive paths while broad/scoped reads remain.
--no-exec false Forbid exec of a new program image after launch (fork/clone still allowed).
--allow-temp profile Also allow writes to the OS temp dir. Requires --write=scope or --write=overlay.
--cpus N Limit CPU usage to N logical cores (fractional allowed, e.g. 1.5). Ignored by Seatbelt.
--memory SIZE Limit memory (512m, 2g, or raw bytes). Ignored by Seatbelt.
--mach-allow NAME Repeatable. Allow a Mach service global-name (Seatbelt only).
--strict false Reject non-portable capabilities.
--dir PATH Working directory for the command; also becomes the default agent writable path.
--backend BACKEND native Force a backend for inspection (seatbelt, gvisor, appcontainer, docker-ephemeral).
--print false Compile and print the plan without running.
--caps false Print the capability matrix and exit.
--version false Print version and build information and exit.

Backend tools and the macOS container workaround are configurable through ISOBOX_SANDBOX_EXEC, ISOBOX_RUNSC, ISOBOX_DOCKER, ISOBOX_DOCKER_IMAGE, and ISOBOX_DOCKER_RUNTIME.

Library

r, err := isobox.New() // native backend for the current GOOS
if err != nil {
	panic(err)
}

spec := isobox.Spec{
	Args:        []string{"just", "build"},
	Net:         isobox.NetDisable,
	Write:       isobox.WriteScope,
	Writable:    []string{"out"},
	CPUs:        1.5,        // cap at 1.5 cores where the backend supports it
	MemoryBytes: 512 << 20,  // cap at 512 MiB; 0 means unlimited
}

plan, _ := r.Compile(spec) // inspect plan.Caveats without running
code, err := r.Run(context.Background(), spec, isobox.Stdio{})
if err != nil {
	panic(err) // failed to launch the sandbox
}
os.Exit(code) // the command's own exit code

The capability tables are plain data, so any backend is inspectable from any host:

isobox.Union()                      // every capability any backend supports
isobox.Intersection()               // capabilities common to all backends
isobox.CapsOf(isobox.BackendGvisor) // one backend's set
isobox.NewBackend(b)                // a runner for a named backend, on any OS

Companion tools

  • cmd/isobox-sshd starts an SSH server inside an isobox so you can shell in and poke at the confinement interactively, the way a remote user would.
  • cmd/isobox-testkit-host and cmd/isobox-testkit-client form an end-to-end test harness that launches a probe inside a sandbox and reports, per capability, whether enforcement actually held.

Notes and caveats

  • --write=overlay is exact on gVisor: --overlay2=root:memory makes writes outside persisted binds ephemeral while --writable paths still hit the host. Seatbelt and AppContainer cannot redirect filesystem writes, so they degrade outside-overlay writes to denial with a plan caveat.
  • gVisor --read-deny obscures existing denied paths with empty bind mounts. Nonexistent denied paths cannot be pre-mounted in broad-read / mode without touching the host.
  • macOS --write=ephemeral clones the workspace with APFS clonefile(2); it protects that workspace, not all of /.
  • --cpus/--memory cap resources on every backend that has a real mechanism: gVisor maps them onto the sandbox's host cgroup via the OCI bundle (runsc needs cgroup support to enforce), docker-ephemeral passes --cpus/--memory to docker run, and AppContainer assigns the process to a Windows job object (whole-job memory cap plus a CPU hard cap scheduled as a share of all host cores). Seatbelt has no resource-limit mechanism, so it reports a caveat and --strict rejects the limits as non-portable.
  • On macOS, docker-ephemeral is an optional workaround for disposable Linux-image runs. Set ISOBOX_DOCKER_IMAGE; it isolates at the VM level unless Docker is configured with a runsc runtime.
  • Seatbelt is covered end-to-end by the test suite where sandbox-exec exists. gVisor, AppContainer, and Docker are covered by compiler unit tests; verify runtime enforcement on a real Linux/Windows host before relying on them.

License

MIT. See LICENSE. Copyright (c) 2026 Can Bölük.

Documentation

Overview

Package isobox runs a command inside the host's native sandbox, behind one interface. On macOS it compiles to a Seatbelt (sandbox-exec) profile; on Linux it compiles to a gVisor (runsc) invocation; on Windows it launches directly inside an AppContainer.

The backends do not enforce the same things. isobox models this explicitly as a capability set per backend, so callers can target either the portable Intersection (behaves identically everywhere) or opt into a backend's Union extras and accept documented, queryable caveats.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Backend

type Backend string

Backend identifies a sandbox implementation.

const (
	// BackendSeatbelt is macOS Seatbelt via sandbox-exec.
	BackendSeatbelt Backend = "seatbelt"
	// BackendGvisor is Linux gVisor via runsc.
	BackendGvisor Backend = "gvisor"
	// BackendAppContainer is Windows AppContainer via in-process Win32 calls.
	BackendAppContainer Backend = "appcontainer"
	// BackendDockerEphemeral is Docker run --rm with no host mounts and a
	// disposable, read-only container filesystem plus tmpfs scratch.
	BackendDockerEphemeral Backend = "docker-ephemeral"
)

func Backends

func Backends() []Backend

Backends returns every backend isobox knows about, sorted.

type Capability

type Capability string

Capability is a single, named sandboxing guarantee. A backend either supports it or it does not; this is what Union and Intersection are computed over.

const (
	// CapNetDisable denies external/non-local network access.
	CapNetDisable Capability = "net.disable"
	// CapNetEnable permits network access.
	CapNetEnable Capability = "net.enable"
	// CapNetOutbound permits outbound connections but blocks listening/inbound.
	CapNetOutbound Capability = "net.outbound"
	// CapFSReadHost grants broad read access to the host filesystem.
	CapFSReadHost Capability = "fs.read.host"
	// CapFSReadScope restricts reads to an explicit allowlist.
	CapFSReadScope Capability = "fs.read.scope"
	// CapFSReadDeny grants broad reads while denying listed sensitive paths.
	CapFSReadDeny Capability = "fs.read.deny"
	// CapFSWriteDeny denies all writes to the host filesystem.
	CapFSWriteDeny Capability = "fs.write.deny"
	// CapFSWriteScope permits writes only to listed paths, persisting to the host.
	CapFSWriteScope Capability = "fs.write.scope"
	// CapFSWriteEphemeral permits writes while arranging for them to be
	// discarded; the host filesystem is never modified.
	CapFSWriteEphemeral Capability = "fs.write.ephemeral"
	// CapProcNoExec forbids creating a new program image after launch.
	CapProcNoExec Capability = "proc.no_exec"
	// CapKernelIsolation serves syscalls from a user-space kernel, shielding the
	// host kernel from the sandboxed process (reduced kernel attack surface).
	CapKernelIsolation Capability = "kernel.isolation"
	// CapIPCRestrict prevents access to host local IPC endpoints.
	CapIPCRestrict Capability = "ipc.restrict"
	// CapMachRestrict is a Seatbelt-only, macOS-specific Mach lookup restriction.
	CapMachRestrict Capability = "mach.restrict"
	// CapResCPU caps CPU usage at a fraction of the host's logical cores.
	CapResCPU Capability = "res.cpu"
	// CapResMemory caps the sandbox's memory footprint in bytes.
	CapResMemory Capability = "res.memory"
)

func (Capability) Describe

func (c Capability) Describe() string

Describe returns a human-readable description of a capability.

type CapabilitySet

type CapabilitySet struct {
	// contains filtered or unexported fields
}

CapabilitySet is an unordered set of capabilities with set algebra. The zero value is not usable; construct with NewCapabilitySet.

func CapsOf

func CapsOf(b Backend) CapabilitySet

CapsOf returns the capability set for a backend. The returned set is a copy.

func Intersection

func Intersection() CapabilitySet

Intersection returns capabilities supported by every backend. A spec built only from these behaves identically on all platforms.

func NewCapabilitySet

func NewCapabilitySet(caps ...Capability) CapabilitySet

NewCapabilitySet builds a set from the given capabilities.

func Union

func Union() CapabilitySet

Union returns capabilities supported by at least one backend.

func (CapabilitySet) Has

func (s CapabilitySet) Has(c Capability) bool

Has reports whether the set contains c.

func (CapabilitySet) Intersect

func (s CapabilitySet) Intersect(other CapabilitySet) CapabilitySet

Intersect returns the capabilities present in both sets.

func (CapabilitySet) Len

func (s CapabilitySet) Len() int

Len reports the number of capabilities in the set.

func (CapabilitySet) List

func (s CapabilitySet) List() []Capability

List returns the capabilities in sorted order for stable output.

func (CapabilitySet) Sub

Sub returns the capabilities in s that are absent from other.

func (CapabilitySet) Union

func (s CapabilitySet) Union(other CapabilitySet) CapabilitySet

Union returns the capabilities present in either set.

type NetMode

type NetMode int

NetMode controls network access. The zero value denies external/non-local access.

const (
	// NetDisable blocks external/non-local network access. Zero value.
	NetDisable NetMode = iota
	// NetEnable permits network access.
	NetEnable
	// NetOutbound permits outbound connections only (no listening/inbound).
	NetOutbound
)

func (NetMode) String

func (n NetMode) String() string

type Plan

type Plan struct {
	// Backend is the implementation that will run the command.
	Backend Backend
	// Argv is the exact process isobox will exec (e.g. sandbox-exec ... or runsc ...).
	Argv []string
	// Profile is the generated Seatbelt SBPL profile, AppContainer preview text,
	// or "" for backends that only have argv.
	Profile string
	// Uses is the set of capabilities this plan actually exercises.
	Uses CapabilitySet
	// Caveats records where the active backend deviates from the spec's intent
	// (degraded or differently-enforced semantics). Empty means a faithful match.
	Caveats []string
	// contains filtered or unexported fields
}

Plan is the compiled, inspectable result of preparing a Spec for a backend. It is produced without running anything, so it can be unit-tested and shown to the user before execution.

func (*Plan) FilesystemVirtualization

func (p *Plan) FilesystemVirtualization() string

FilesystemVirtualization returns the planned filesystem virtualization mechanism, or "" when the plan uses no extra filesystem layer.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner compiles specs for one backend and runs them.

func New

func New() (*Runner, error)

New returns a Runner for the current OS, or an error on unsupported platforms.

func NewBackend

func NewBackend(b Backend) (*Runner, error)

NewBackend returns a Runner for a specific backend regardless of host OS. This lets you compile and inspect a plan for a non-native backend (e.g. preview the Windows AppContainer grants from a Mac). Running it still requires the native backend to exist on the host.

func (*Runner) Backend

func (r *Runner) Backend() Backend

Backend reports which backend this runner uses.

func (*Runner) Capabilities

func (r *Runner) Capabilities() CapabilitySet

Capabilities reports what this runner's backend can enforce.

func (*Runner) Compile

func (r *Runner) Compile(s Spec) (*Plan, error)

Compile prepares a Spec into an inspectable Plan without running anything.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, s Spec, streams Stdio) (int, error)

Run compiles the spec and executes it, returning the command's exit code. A non-nil error means isobox failed to launch the sandbox; a command that runs and exits non-zero returns that code with a nil error.

type Spec

type Spec struct {
	// Args is the command and its arguments. Args[0] is the executable.
	Args []string
	// Dir is the working directory for the command. Empty inherits the caller's.
	Dir string
	// Env is the environment as KEY=VALUE entries. Nil inherits the caller's.
	Env []string

	// Net selects the network policy.
	Net NetMode
	// Write selects the filesystem write policy.
	Write WriteMode
	// Writable lists paths the sandbox may write when Write == WriteScope or
	// Write == WriteOverlay.
	Writable []string
	// Readable, when non-empty, restricts reads to these paths (plus the
	// minimum needed to load the executable). Empty grants broad host reads.
	Readable []string
	// ReadDeny lists sensitive paths to carve out of broad/scoped reads when a
	// backend supports read deny rules.
	ReadDeny []string
	// NoExec forbids creating a new program image after the initial command
	// starts. It does not forbid fork/clone without exec.
	NoExec bool
	// AllowTemp additionally permits writes to the OS temp dir when
	// Write == WriteScope or Write == WriteOverlay. Many tools need a writable
	// temp; it is opt-in except profiles may enable it.
	AllowTemp bool

	// MachAllow lists Mach service global-names the sandbox may look up despite
	// the default Mach-lookup denial. It is a Seatbelt-only allowance
	// (mach.restrict); other backends ignore it with a caveat. Use it
	// for macOS programs that need directory services — for example
	// "com.apple.system.opendirectoryd.libinfo" so getpwnam-based user lookup
	// resolves names instead of falling back to raw uids.
	MachAllow []string

	// CPUs limits CPU usage to this many logical cores; fractional values are
	// allowed (1.5 = one and a half cores). Zero means no CPU limit. Backends
	// that cannot cap CPU report a caveat (and Strict rejects it).
	CPUs float64
	// MemoryBytes limits the sandbox's memory footprint, in bytes. Zero means
	// no memory limit. Backends that cannot cap memory report a caveat (and
	// Strict rejects it).
	MemoryBytes int64

	// Strict rejects any capability outside Intersection(), guaranteeing
	// identical enforcement across backends instead of degrading per platform.
	Strict bool
}

Spec is a backend-independent description of a confined command. Backends do not share the same capability set, so non-portable choices are reported as caveats, or rejected outright when Strict is set.

func (Spec) Capabilities

func (s Spec) Capabilities() CapabilitySet

Capabilities returns the capability set this spec requests.

type Stdio

type Stdio struct {
	In  io.Reader
	Out io.Writer
	Err io.Writer
}

Stdio wires the sandboxed process's standard streams. The zero value uses the caller's os.Stdin/Stdout/Stderr.

type WriteMode

type WriteMode int

WriteMode controls filesystem writes. The zero value denies all writes.

const (
	// WriteNone denies every write to the host filesystem. Zero value.
	WriteNone WriteMode = iota
	// WriteScope permits writes under Spec.Writable and optional temp roots.
	WriteScope
	// WriteEphemeral permits writes anywhere but discards them; host untouched.
	WriteEphemeral
	// WriteOverlay persists writes under Spec.Writable and optional temp roots,
	// while writes elsewhere are ephemeral when the backend can provide an
	// overlay/shadow filesystem and denied otherwise.
	WriteOverlay
)

func (WriteMode) String

func (w WriteMode) String() string

Directories

Path Synopsis
cmd
isobox command
Command isobox runs a command inside a host-native or portability-workaround sandbox backend: Seatbelt on macOS, gVisor on Linux, AppContainer on Windows, and optional ephemeral container backends where available.
Command isobox runs a command inside a host-native or portability-workaround sandbox backend: Seatbelt on macOS, gVisor on Linux, AppContainer on Windows, and optional ephemeral container backends where available.
isobox-sshd command
Command isobox-sshd launches an SSH server inside an isobox sandbox so you can ssh in and explore the confinement interactively, the way a remote user would.
Command isobox-sshd launches an SSH server inside an isobox sandbox so you can ssh in and explore the confinement interactively, the way a remote user would.
internal
reslimit
Package reslimit parses human-friendly CPU and memory limit strings into the numeric values isobox.Spec expects.
Package reslimit parses human-friendly CPU and memory limit strings into the numeric values isobox.Spec expects.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL