sandbox

package
v0.48.1 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package sandbox is a spike (MCP-3232) proving an *unprivileged*, non-Docker confinement primitive for stdio MCP servers on hosts where Docker is unavailable or broken — notably Ubuntu 24.04 with snap-installed Docker under AppArmor, where the deb's systemd hardening fights snap-confine (see internal repo issue #457 / cmd/mcpproxy/doctor_env_snapdocker.go).

The mechanism chosen by the spike is the Linux Landlock LSM (kernel 5.13+). Unlike user-namespace / bubblewrap sandboxes, Landlock does NOT require unprivileged user namespaces and is therefore NOT blocked by `kernel.apparmor_restrict_unprivileged_userns=1`, which Ubuntu 23.10+/24.04 enable by default. See docs/development/sandbox-spike-mcp-34.md for the full recommendation and the honest limits (no uid/gid separation without privilege, filesystem-allowlist + rlimits only).

This package is intentionally minimal: it confines the *current* process and, because Landlock domains are inherited across execve, every child it then execs (the npx/uvx server and its descendants). The intended integration is a tiny re-exec wrapper that calls Apply and then execs the untrusted command; the package test exercises exactly that shape.

Index

Constants

View Source
const (
	// Subcommand is the hidden mcpproxy subcommand that runs the re-exec child.
	Subcommand = "__sandbox_exec"

	// EnvSpec carries the JSON-encoded Spec from the parent to the re-exec child.
	EnvSpec = "MCPPROXY_SANDBOX_SPEC"
)

Re-exec wrapper protocol (MCP-34.3).

Landlock confines the *current* process and every process it then execs, and the confinement is irreversible — so it cannot be applied in-process before mcp-go spawns an upstream stdio server. The integration is therefore a tiny re-exec wrapper: mcpproxy launches itself as

mcpproxy __sandbox_exec -- <command> [args...]

with the desired confinement encoded in the environment. The child decodes the Spec, calls Apply, then execs <command>, replacing its own image so the untrusted server inherits the locked-down Landlock domain and the server's stdin/stdout/stderr pass straight through with no intervening mux.

Variables

View Source
var ErrUnsupported = errors.New("sandbox: confinement primitive unavailable on this platform/kernel")

ErrUnsupported is returned by Apply on platforms or kernels that do not provide the requested confinement primitive (e.g. non-Linux, or a kernel without Landlock). Callers that want fail-open behaviour set Spec.BestEffort.

Functions

func Available added in v0.47.0

func Available() bool

Available reports whether the native sandbox primitive (Landlock) can be enforced on this kernel right now. It lets callers log an honest diagnostic ("sandbox requested but kernel lacks Landlock; running degraded") and lets tests skip enforcement assertions on kernels without Landlock.

func RunChild added in v0.47.0

func RunChild(argv []string, diag io.Writer) int

RunChild is the entrypoint for the hidden `__sandbox_exec` subcommand. argv is the command line after the `--` separator: argv[0] is the program to run and argv[1:] are its arguments. It decodes the Spec from EnvSpec, applies the confinement, performs a best-effort uid/gid drop, then execs the target, replacing this process image — so the untrusted server inherits the locked Landlock domain and its stdin/stdout/stderr stay wired straight through to the parent with no mux.

On success it never returns (execve replaces the image). It returns a non-zero exit code on any failure; diag receives human-readable confinement notes and errors (os.Stderr in production, so they land in the per-server upstream log).

func WrapCommand added in v0.47.0

func WrapCommand(self string, spec Spec, command string, args []string) (wrappedCommand string, wrappedArgs []string, extraEnv []string, err error)

WrapCommand builds the argv and extra environment needed to launch command/args confined by spec, by re-executing self (the absolute path to the running mcpproxy binary) as the sandbox child. The returned extraEnv entries must be appended to the child process's environment so SpecFromEnv can decode the Spec on the other side.

It performs no syscalls and is safe to call on every platform; whether the confinement actually takes effect is decided later by Apply inside the child (a no-op on non-Linux / Landlock-less kernels).

Types

type Report

type Report struct {
	// LandlockABI is the kernel's reported Landlock ABI version that was
	// enforced (>=1), 0 if Landlock was not requested, or -1 if Landlock is
	// unavailable on this kernel/platform.
	LandlockABI int
	// LandlockNote is a human-readable note (e.g. why Landlock was skipped, or
	// which paths were missing).
	LandlockNote string
	// RlimitsSet is the number of rlimits successfully applied.
	RlimitsSet int
	// NoNewPrivs reports whether PR_SET_NO_NEW_PRIVS was set (always true when
	// Landlock is enforced; Landlock requires it).
	NoNewPrivs bool
}

Report records what Apply actually enforced, so callers can log an honest account of the confinement (important because Apply is best-effort across kernels with different Landlock ABI levels).

func Apply

func Apply(spec Spec) (Report, error)

Apply confines the current process per spec. On success the calling process — and every process it subsequently execs — can only touch the filesystem subtrees in the allowlist, under the supplied rlimits. The restriction is irreversible for the lifetime of the process, which is why the intended caller is a short-lived re-exec wrapper that calls Apply and immediately execs the untrusted command.

type Rlimit

type Rlimit struct {
	Resource int
	Cur      uint64
	Max      uint64
}

Rlimit is a single setrlimit request. Resource is one of the unix.RLIMIT_* constants (e.g. RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_CPU).

type Spec

type Spec struct {
	// ReadOnlyPaths are filesystem subtrees the confined process may read and
	// execute. Anything not covered by a ReadOnlyPaths/ReadWritePaths entry is
	// denied. Missing paths are skipped (best-effort) and noted in the Report.
	ReadOnlyPaths []string

	// ReadWritePaths are subtrees the confined process may read, execute, and
	// write (create/delete/truncate within).
	ReadWritePaths []string

	// Rlimits are resource limits applied via setrlimit before confinement.
	Rlimits []Rlimit

	// BestEffort, when true, downgrades "primitive unavailable" from an error
	// to a no-op recorded in Report.LandlockNote, mirroring go-landlock's
	// BestEffort semantics. When false (the default, fail-closed), Apply
	// returns ErrUnsupported if Landlock cannot be enforced — a security
	// boundary should fail closed rather than silently run unconfined.
	BestEffort bool
}

Spec describes the confinement to apply to the current process before it execs an untrusted stdio MCP server. The zero value applies no filesystem restriction (only rlimits, if any).

func SpecFromEnv added in v0.47.0

func SpecFromEnv() (spec Spec, ok bool, err error)

SpecFromEnv decodes the Spec the parent encoded into EnvSpec. ok is false when the variable is absent — i.e. the process was not launched as a sandbox child.

Jump to

Keyboard shortcuts

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