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
- func Ancestors(paths ...string) []string
- func Available() (string, bool)
- func Build(in Input) (*Policy, []Refusal)
- func ChildEnv(env []string, tmp string) []string
- func DarwinProfile(p *Policy) (text string, params []string, err error)
- func DroppedEnv(env []string) []string
- func Inside(path, dir string) bool
- func NetEnforceable() bool
- func Note() string
- func OptionalRoots(command string) []string
- func Run(p *Policy, env []string, stdin io.Reader, stdout, stderr io.Writer, ...) (int, error)
- type Input
- type Policy
- type Refusal
Constants ¶
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.
const ABI = "-"
ABI is the abi= field; there is no backend to have one.
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 ¶
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 Build ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 OptionalRoots ¶
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).
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.
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 ¶
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.