sandbox

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package sandbox defines the openblox contract for running untrusted, machine-generated code in disposable Linux sandboxes.

This package holds only interfaces, value types, options, and errors. Implementations live in sibling packages — see github.com/blox-eng/openblox/pkg/docker for the Docker + gVisor backend.

Secure by default

The zero value of every option is the safe one. A sandbox created with no options has no network, dropped capabilities, a read-only root filesystem, a non-root user, and bounded CPU, memory, and process count:

sb, err := backend.Create(ctx, "session-1")                     // locked down
sb, err := backend.Create(ctx, "session-1", WithEgress(policy)) // deliberately not

This inverts the common design where a permissive default is hardened by remembering the right flags. Forgetting an option here can only make a sandbox more restrictive, never less.

Naming and identity

Sandboxes are addressed by a caller-supplied name, and two calls with the same name reach the same sandbox until it is reaped. openblox does not interpret the name: multi-tenancy, per-user scoping, and access control belong to the caller. Callers separating untrusted principals should hash their identity into the name rather than passing it through, since the name reaches the container runtime.

Index

Constants

View Source
const (
	DefaultRuntime     = "runsc"
	DefaultUser        = "1000:1000"
	DefaultCPUs        = 2
	DefaultMemoryBytes = 2 << 30 // 2 GiB
	// DefaultDiskBytes is scratch space, drawn from the memory budget because it
	// is tmpfs. Half of memory leaves room for the processes doing the writing.
	DefaultDiskBytes      = 1 << 30 // 1 GiB
	DefaultMaxProcesses   = 256
	DefaultIdleTimeout    = 15 * time.Minute
	DefaultMaxAge         = 2 * time.Hour
	DefaultCommandTimeout = 60 * time.Second
	// MaxCommandTimeout bounds what a caller may request. Long enough for heavy
	// domain compute (large-model geometry, big spreadsheet parses) without
	// letting a wedged call hold a slot indefinitely.
	MaxCommandTimeout = 10 * time.Minute
)

Defaults for a sandbox created with no options. Conservative on purpose: raising a limit is a visible edit at the call site, and a caller who forgets an option gets the restrictive behaviour rather than the permissive one.

Variables

View Source
var (
	// ErrNotFound means no sandbox exists under the requested name.
	ErrNotFound = errors.New("sandbox not found")

	// ErrInvalid means the request was malformed and retrying will not help.
	ErrInvalid = errors.New("invalid request")

	// ErrTimeout means a command exceeded its deadline and was killed. The
	// sandbox itself remains usable.
	ErrTimeout = errors.New("timed out")

	// ErrRuntimeUnavailable means the host cannot provide the required
	// isolation — typically that the gVisor runtime is not installed.
	//
	// Backends must fail with this rather than silently falling back to weaker
	// isolation. A sandbox that is quietly less isolated than requested is worse
	// than no sandbox, because the caller keeps trusting it.
	ErrRuntimeUnavailable = errors.New("required runtime unavailable")

	// ErrImageUnavailable means the sandbox image could not be obtained: it is
	// absent locally and could not be pulled. Distinct from ErrInvalid because
	// the request was well-formed and retrying may well succeed — a registry
	// being unreachable is a different problem from a misspelled reference.
	ErrImageUnavailable = errors.New("sandbox image unavailable")
)

Sentinel errors. Match them with errors.Is; backends wrap them with context rather than returning them bare.

Functions

This section is empty.

Types

type Backend

type Backend interface {
	// Create returns a running sandbox for name, creating it if absent and
	// returning the existing one if present. It is the caller's session-affinity
	// primitive: the same name reaches the same sandbox until it is reaped.
	//
	// Options are applied only when the sandbox is created. Calling Create again
	// with different options on a live sandbox does not reconfigure it.
	Create(ctx context.Context, name string, opts ...CreateOption) (Sandbox, error)

	// Open returns an existing sandbox. It reports ErrNotFound if name has no
	// sandbox, and never creates one.
	Open(ctx context.Context, name string) (Sandbox, error)

	// List returns every sandbox this backend manages, running or stopped.
	List(ctx context.Context) ([]Info, error)

	// Destroy removes a sandbox and its writable layer. It is idempotent:
	// destroying an absent sandbox is not an error.
	Destroy(ctx context.Context, name string) error

	// Close releases the backend's own resources. It does not stop or destroy
	// running sandboxes, which outlive the process that created them.
	Close() error
}

Backend provisions sandboxes on a host runtime. Implementations are safe for concurrent use.

type Command

type Command struct {
	// Argv is the program and its arguments. It is executed directly, without a
	// shell, so no quoting or escaping is applied. To use shell syntax, invoke a
	// shell explicitly: []string{"sh", "-c", script}.
	Argv []string
	// Env entries are "KEY=value", merged over the sandbox's environment.
	Env []string
	// Dir is the working directory. Empty means the image's default.
	Dir   string
	Stdin io.Reader
	// Timeout bounds this command. Zero means the backend's default. The backend
	// clamps it to a configured maximum.
	Timeout time.Duration
}

Command is a program to run inside a sandbox.

There is no "language" field. A sandbox runs commands; mapping a language to an interpreter invocation belongs to the caller, who knows what its image contains.

func (Command) Validate

func (c Command) Validate() error

Validate reports whether the command is runnable.

type CreateOption

type CreateOption func(*Spec)

CreateOption customises a sandbox at creation.

func WithCommandTimeouts

func WithCommandTimeouts(def, ceiling time.Duration) CreateOption

WithCommandTimeouts overrides the default and maximum command durations.

func WithEgress

func WithEgress(p EgressPolicy) CreateOption

WithEgress relaxes the default of no network access.

func WithEnv

func WithEnv(entries ...string) CreateOption

WithEnv appends environment entries, each "KEY=value".

func WithImage

func WithImage(ref string) CreateOption

WithImage sets the container image. Pin a digest rather than a tag: a tag can be repointed under you, and the image is the sandbox's entire userland.

func WithLabel

func WithLabel(key, value string) CreateOption

WithLabel attaches a label for the caller's own bookkeeping. Labels are visible to host tooling and must not carry secrets.

func WithLifetime

func WithLifetime(l Lifetime) CreateOption

WithLifetime overrides the idle and max-age bounds. A zero field keeps its default; use a negative value to disable a bound entirely.

func WithResources

func WithResources(r Resources) CreateOption

WithResources overrides the resource bounds. Zero-valued fields keep their defaults, so a caller can raise memory alone without unbounding the rest.

func WithRuntime

func WithRuntime(name string) CreateOption

WithRuntime overrides the isolation runtime. The default, runsc, is gVisor. Setting this to the host's default runtime trades away the isolation openblox exists to provide.

func WithUser

func WithUser(user string) CreateOption

WithUser sets the uid:gid the sandbox runs as. It must not be root.

type EgressPolicy

type EgressPolicy int

EgressPolicy controls a sandbox's outbound network access.

const (
	// EgressNone gives the sandbox no network interface at all. It is the zero
	// value, and therefore the default.
	//
	// This is stronger than a firewall that drops outbound packets: with no
	// interface there is no DNS resolver to abuse as a covert channel, and no
	// route to host-local metadata services. Files still move in and out over
	// the runtime's control channel, so default-deny costs nothing functionally.
	EgressNone EgressPolicy = iota

	// EgressUnrestricted gives the sandbox ordinary host networking.
	//
	// Only appropriate when the code being run is trusted. If it is trusted, ask
	// why it needs a sandbox.
	EgressUnrestricted
)

type Info

type Info struct {
	// Name is the caller-supplied identity passed to Create.
	Name string
	// ID is the runtime's identifier, for correlating with host-level tooling.
	ID    string
	Image string
	State State

	CreatedAt time.Time
}

Info describes a sandbox.

type Lifetime

type Lifetime struct {
	// IdleTimeout stops a sandbox with no activity for this long.
	IdleTimeout time.Duration
	// MaxAge destroys a sandbox this long after creation regardless of activity.
	// It reaps what the idle sweep misses — a sandbox kept warm by a wedged
	// background process is never idle.
	MaxAge time.Duration
}

Lifetime bounds how long a sandbox may live.

type Preview

type Preview struct {
	URL   string
	Token string
	// Port is echoed back so a caller holding several previews can match them
	// up without tracking the mapping itself.
	Port      int
	ExpiresAt time.Time
}

Preview is a signed, expiring route to a port inside a sandbox.

func (Preview) Expired

func (p Preview) Expired(now time.Time) bool

Expired reports whether the preview is no longer valid as of now.

type Resources

type Resources struct {
	CPUs        float64
	MemoryBytes int64

	// DiskBytes caps the sandbox's writable scratch space.
	//
	// The root filesystem is read-only, so all writes land on sized tmpfs mounts
	// and this is their combined ceiling. Container-layer quotas
	// (--storage-opt size) are deliberately not used: they require overlay2 on
	// xfs with pquota and hard-fail on the far more common ext4, so relying on
	// them would make the disk bound silently unavailable on most hosts.
	//
	// Because tmpfs is RAM-backed, this budget is drawn from MemoryBytes rather
	// than being independent of it: a sandbox that fills its scratch space has
	// that much less memory for processes. DiskBytes must not exceed
	// MemoryBytes.
	DiskBytes int64

	// MaxProcesses caps the process count. Without it a fork bomb inside the
	// sandbox exhausts host PIDs regardless of CPU and memory limits.
	MaxProcesses int
}

Resources bounds what a single sandbox may consume.

func (Resources) Validate

func (r Resources) Validate() error

Validate reports whether the bounds are internally consistent.

type Result

type Result struct {
	Stdout   []byte
	Stderr   []byte
	ExitCode int
}

Result is the outcome of a completed command.

Stdout and Stderr are bytes, not strings: a sandbox runs arbitrary programs and its output is not guaranteed to be valid UTF-8.

type Sandbox

type Sandbox interface {
	// Info returns a snapshot of the sandbox's identity and state. It does not
	// query the runtime and so cannot fail; use [Backend.Open] for fresh state.
	Info() Info

	// Exec runs a command to completion and returns its output. A non-zero exit
	// status is reported in Result.ExitCode, not as an error — err is non-nil
	// only when the command could not be run or did not finish.
	Exec(ctx context.Context, cmd Command) (Result, error)

	// WriteFile writes src to path inside the sandbox, creating parent
	// directories as needed. It streams, so it is safe for large payloads.
	WriteFile(ctx context.Context, path string, mode fs.FileMode, src io.Reader) error

	// ReadFile opens path inside the sandbox for reading. The caller must close
	// the returned reader.
	ReadFile(ctx context.Context, path string) (io.ReadCloser, error)

	// StartProcess starts cmd as a detached background process under name. It is
	// idempotent: if a process is already running under name, it is left alone
	// and no error is returned.
	StartProcess(ctx context.Context, name string, cmd Command) error

	// Expose returns a signed, expiring URL for a port inside the sandbox.
	//
	// The returned token is a bearer credential and must be sent as a request
	// header, never a query parameter — query strings leak into access logs,
	// browser history, and Referer headers.
	Expose(ctx context.Context, port int, ttl time.Duration) (Preview, error)

	// Revoke invalidates a token returned by Expose before it expires.
	Revoke(ctx context.Context, port int, token string) error

	// Stop halts the sandbox without discarding it. A stopped sandbox can be
	// reached again through [Backend.Create] with the same name.
	Stop(ctx context.Context) error
}

Sandbox is a live instance. Methods are safe for concurrent use, but the guest they talk to is not: concurrent Exec calls run concurrently inside it.

type Spec

type Spec struct {
	Image     string
	Runtime   string
	User      string
	Resources Resources
	Lifetime  Lifetime
	Egress    EgressPolicy
	Env       []string
	Labels    map[string]string

	// DefaultTimeout applies to commands that set none.
	DefaultTimeout time.Duration
	// MaxTimeout is the ceiling a command may request.
	MaxTimeout time.Duration
}

Spec is the resolved configuration for a new sandbox. Callers build one through CreateOption values rather than constructing it directly.

func NewSpec

func NewSpec(opts ...CreateOption) Spec

NewSpec resolves options over the secure defaults.

func (Spec) ResolveTimeout

func (s Spec) ResolveTimeout(requested time.Duration) time.Duration

ResolveTimeout returns the effective timeout for a requested duration, substituting the default when unset and clamping to the maximum.

type State

type State string

State is the lifecycle position of a sandbox.

openblox deliberately models three states rather than tracking every transition: the container runtime is the authority, and a parallel state machine over it can only drift.

const (
	// StateRunning means the sandbox is up and accepting commands.
	StateRunning State = "running"
	// StateStopped means the sandbox exists but is halted. It can be restarted
	// through [Backend.Create] with the same name.
	StateStopped State = "stopped"
	// StateError means the runtime reported a state openblox cannot act on. The
	// sandbox should be destroyed rather than reused.
	StateError State = "error"
)

The states a sandbox can be in.

Jump to

Keyboard shortcuts

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