sandbox

package
v0.15.2 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package sandbox is the wall of docs/SPEC-SANDBOX.md: one command run with its filesystem reach cut down by the operating system. This file is the platform-independent half — the Policy, the path resolution and refusal of rule 5, the per-platform root tables as data, and the temp directory of rule 8. The three bodies that apply a policy live behind build tags beside it, and on a platform whose backend is not built the body REFUSES (rule 1): there is no fallback, no degraded mode, and no partial wall.

Every platform whose body is not built yet REFUSES, and this file is that refusal. Rule 1 is "OS-enforced or refused": a stub that proceeded would be the silent sandbox the whole tool exists to prevent, so the linux (Landlock) and windows (AppContainer) bodies named in docs/SPEC-SANDBOX.md are not stubbed as pass-throughs — they are stubbed as NO, and the refusal names the platform so that a reader knows which body is missing rather than that "the sandbox failed".

Index

Constants

View Source
const (
	ExitRefused     = 125 // nova-sandbox itself said NO before the command ran
	ExitNotExecuted = 126 // the command could not be executed and the tool was still there
	ExitNotFound    = 127 // the command could not be resolved on the caller's PATH
	ExitProbeFailed = 1   // probe/check grammar: the verb ran and said NO
	ExitCannotRun   = 2   // probe/check grammar: the verb could not run

)

Exit codes. The exec path uses the env(1)/timeout(1) convention rather than SPEC.md's 0/1/2, because its status belongs to the wrapped command; the departure and its reason are in the spec's exit-codes section.

View Source
const ABI = "-"

ABI is the abi= field; there is no backend to have one.

View Source
const Backend = "none"

Backend is what the SANDBOX OK line would name here — and no line here says OK.

Variables

This section is empty.

Functions

func Ancestors

func Ancestors(paths ...string) []string

Ancestors is every proper ancestor of the given paths, "/" excluded, sorted and unique. It is what the darwin profile's file-read-metadata literals are built from, and it is here rather than in the darwin body so that a test on any platform can assert its shape.

func Available

func Available() (string, bool)

Available is rule 1's question, and on a platform with no body the answer is no.

func Build

func Build(in Input) (*Policy, []Refusal)

Build turns an Input into a Policy, or into every independent refusal it holds. It creates exactly one directory, rule 8's, and only when the rest of the input is sound.

func ChildEnv

func ChildEnv(env []string, tmp string) []string

ChildEnv is rule 9's environment: the caller's, unchanged, minus the three temp variables the tool sets to rule 8's directory — and minus the agent sockets. It is not a secrets tool: the credential the caller deliberately passed by environment must arrive, and every other variable passes through untouched.

The agent variables are the exception, and they are a fix this build made to the spec rather than something the spec asked for. SSH_AUTH_SOCK names a unix-domain socket that speaks for a private key without ever revealing it: a wall that denies ~/.ssh but leaves the agent reachable has not stopped the thing ~/.ssh was about. The profile denies unix sockets outside the write set, which is the wall; unsetting the variables is the fence beside it, so that an honest program does not try and a log does not have to be read to see that it could not.

func DarwinProfile

func DarwinProfile(p *Policy) (text string, params []string, err error)

DarwinProfile fills the template for ONE run and returns the profile text and the -D parameters that must accompany it. Caller paths never enter the text as data: they arrive as parameters and are read back as (param "READn"), (param "WRITEn") and (param "HOME"), so a directory name cannot rewrite the policy. The one place a path does enter the text is the ancestor literals, which is why Build refuses a path carrying an SBPL metacharacter.

Every param the filled profile names MUST be passed: an unfilled (param ...) is "invalid data type of path filter; expected pattern, got boolean" at exit 65, which is a broken run and not a weaker wall. The params returned here are exactly the params the returned text names.

func DroppedEnv

func DroppedEnv(env []string) []string

DroppedEnv names the variables ChildEnv removes that are not the three temp ones, for the one NOTE line the tool prints before the command starts. A reader of a log should not have to diff two environments to learn that the agent was taken away.

func Inside

func Inside(path, dir string) bool

Inside reports whether path is dir or lies beneath it. Both are expected resolved.

AND "BENEATH" IS A QUESTION FOR THE FILESYSTEM, NOT FOR A STRING PREFIX (#145). This was `strings.HasPrefix`, a case-SENSITIVE comparison, and APFS is case-INsensitive by default (NTFS too): a `--secret` spelled in another case than the `--read` it actually sits inside passed rule 6's own `secret_inside_allow` check and the probe reported a pass, and a `HOME` inside a `--write` under a spelling the filesystem folds was refused `home_outside` -- the same fold, read the other way about, refusing a configuration that is sound. `filepath.EvalSymlinks` does not fold case on darwin, so a resolved path does not close it, and lowercasing is not the repair: on a case-SENSITIVE filesystem `/x/Read` and `/x/read` are two directories and folding them would answer a neighbour wrong.

So the answers, in order:

  • `path == dir` is inside, which is this function's own contract and the swarm's `insideDir` differs from it deliberately;
  • the string prefix stays as the CHEAP first answer, where it says yes it is right;
  • where it says no and dir EXISTS, `os.SameFile` against path and each of its ancestors that exists -- device and inode is the question the filesystem itself answers, so it holds for a case fold, for one directory mounted at two names and for a hard-linked directory. The walk starts at PATH, not its parent, because a path that IS dir under another spelling is inside it by the contract above. When dir exists this walk is the whole answer: an ancestor of path at dir's own depth either is dir or is not;
  • where dir is NOT there, nothing has an inode and the name is all there is: the prefix again, case-insensitively, and only where the filesystem is MEASURED to fold (dirFoldsCase, never runtime.GOOS). Every caller path of this package exists by rule 5, so this last answer is defence in depth.

This runs while the policy is built, never per operation inside the wall.

func NetEnforceable

func NetEnforceable() bool

NetEnforceable: an enforced network denial needs a backend first.

func Note

func Note() string

Note is the one clause the check verb prints about this machine.

func OptionalRoots

func OptionalRoots(command string) []string

OptionalRoots is the machine's answer to the table above plus the directory of the resolved command, which is a root for exactly this run (the spec's roots table names it on all three platforms).

func Run

func Run(p *Policy, env []string, stdin io.Reader, stdout, stderr io.Writer, okLine func()) (int, error)

Run refuses, and the command does not run. It never returns a zero status.

Types

type Input

type Input struct {
	Reads     []string
	Writes    []string
	Cwd       string // empty: the first --write (rule 13)
	Tmp       string // empty: <first --write>/.nova-sandbox-tmp (rule 8)
	Name      string // windows container name; accepted and ignored elsewhere
	NetDeny   bool
	NetListen bool
	Argv      []string // the command and its arguments, everything after --
	Home      string   // the caller's HOME as the child will see it (rule 9)
	LookAt    string   // PATH to resolve the command on; empty means the process's own
}

Input is the argv as the caller typed it, before any resolution. Everything here is a claim about this job; Build turns it into a Policy or into refusals.

type Policy

type Policy struct {
	Reads     []string // resolved, read-only, recursive
	Writes    []string // resolved, read+write, recursive; the first is load-bearing
	OptRoots  []string // the platform's optional roots that EXIST on this machine
	Cwd       string
	Tmp       string
	Home      string
	Name      string
	NetDeny   bool
	NetListen bool
	Command   string   // the resolved absolute path of the executable
	Argv      []string // Command followed by its arguments, verbatim

	// Extra is the file descriptors the child gets ABOVE stdin/stdout/stderr, in order,
	// starting at fd 3. It is never built from caller input: Build leaves it nil and the
	// only writer is the probe, which hands its child one end of a pipe carrying the
	// one-time value that makes the child the probe's own (cmd/nova-sandbox/main.go).
	// A descriptor cannot be forged by a caller who merely knows an argument, which is
	// why the probe's guard stands on one.
	Extra []*os.File
}

Policy is one run's wall: resolved, absolute, existing paths and nothing guessed. The two named exceptions to "never guessed" are rule 4's, and both are recorded here as the caller's own first --write.

func (*Policy) CmdName

func (p *Policy) CmdName() string

CmdName is the base name of the executable, and it is the ONLY thing about the argv that is ever printed: arguments carry task text and task text carries quoted rules.

func (*Policy) Net

func (p *Policy) Net() string

Net is the word rule 7 puts on the SANDBOX OK line. There is no net=unenforced: a denial that cannot be enforced is a refusal, not a word in a line.

type Refusal

type Refusal struct {
	Reason string // the reason= token of the SANDBOX REFUSED line
	Text   string // what was wrong and what the flag wants, never a file's contents
}

Refusal is one independent problem, named by the flag it is about. Rule 16: a refusal says what the input wants and every independent problem is reported at once, so the callers below collect these rather than returning the first.

func ResolveCallerFile

func ResolveCallerFile(flag, raw string) (string, *Refusal)

ResolveCallerFile is rule 5 for a caller path that names a FILE rather than a directory: --secret is the only one, and before this it was the one caller path the tool never resolved and never metacharacter-checked. It is absolute, it exists, it is not a directory, its symlinks are followed, and it carries nothing the generated policy cannot. A path that does not exist is a refusal, because a probe that "could not read" a file that was never there is a pass about nothing.

func (Refusal) Code

func (r Refusal) Code() int

Code is the exit status this refusal costs. Every refusal of the tool's own is 125 except the one about a command that is not on the PATH at all.

func (Refusal) Error

func (r Refusal) Error() string

Jump to

Keyboard shortcuts

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