sandbox

package
v1.2.9 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

The egress wall is the card's OUTBOUND half: the filesystem wall this package builds everywhere else says what a card may read and write, and this file says what it may talk to. The coordinator's design page of 2026-09-18 is the contract:

Where: nftables on the bench, applied to the slirp/pasta veth of each `podman run`.
Not an env list the worker applies (the worker is the adversary). Not --network=host.
Policy file in git: infra/image/egress.txt (one hostname per line, comments #).
Default deny. Allow TCP 443 only: github.com, api.github.com,
objects.githubusercontent.com, the one model host named in the worker description
(resolved at run start, pinned for the run). Deny: 169.254.169.254/32, 127.0.0.0/8 as
a destination, other benches, UDP except DNS to the resolver for those names.

Three things in this file are the page's silences, read the SAFER way and said out loud here and in docs/SPEC-SANDBOX.md, because a silence read the loose way is a hole:

  1. `--model-host` may only name a host the policy file already carries. The page says an update is "a PR to egress.txt, reviewed by this sitting. Not a runtime flag", and a flag that could name ANY host would be exactly that runtime flag. The file is the reviewed universe; the flag picks the ONE model host out of it for this run.
  2. A pinned address inside a denied range — loopback, link-local, a bench — refuses the whole plan. That answer comes from a resolver, the resolver is not ours, and a name that resolves to 127.0.0.1 is either poisoned or a rebinding. Fail closed.
  3. Every rule is scoped to the card's own traffic (`meta skuid <n>` or `iifname "<veth>"`) and a plan with neither selector REFUSES. An unscoped default deny in the output hook would firewall the bench itself, which is a far worse failure than the one it was meant to prevent.

Nothing here executes anything: BuildEgress renders text, and the tool's egress verbs are what hand that text to nft. The resolver is an interface so the tests pin addresses without a packet (unit tests never touch the network).

Local GPU capability for docs/SPEC-SANDBOX.md, nova-tools #230.

The only explicit capability is `--gpu none|metal` (default none). Opting in records intent on the SANDBOX OK line; it never widens mach-lookup and never grants blanket device access.

The Landlock ABI, as data and in one place. This file knows the kernel's numbers and nothing about this tool's policy; wrap_linux.go turns a Policy into a ruleset with it.

docs/SPEC-SANDBOX.md, "Linux — Landlock, no root" is normative for every table here. The one rule that governs the whole file: an access this tool does not HANDLE is an access the kernel does not CHECK, and that is a hole with no line in any log. So the handled set is the whole set the discovered ABI defines, and an ABI above the highest row of the table below is REFUSED rather than guessed at.

The landlock ruleset as printable text: what `policy` writes on this platform instead of the darwin profile, which no linux run uses (issue #1469).

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, path resolution and refusal of invalid paths, per-platform root tables as data, and the temporary directory. 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: there is no fallback, no degraded mode, and no partial wall.

The linux body: Landlock, the LSM an unprivileged process can apply to itself, which the kernel inherits across fork(2) and execve(2) so that the command cannot lift it. docs/SPEC-SANDBOX.md, "Linux — Landlock, no root" is normative.

THE SHAPE IS RESTRICT-THEN-FORK, AND IT IS NOT THE SHAPE THE SPEC'S PROPOSAL NAMED. The proposal was restrict-then-exec IN PLACE: the tool applies the ruleset to itself and then syscall.Exec's the command, becoming it, so Run never returns. That cannot be this body, and the reason is `probe`: probeVerb runs FOUR walled steps in ONE process and reads the status of each (cmd/nova-sandbox/main.go, walled()), so a Run that never returns turns the probe into its own first step and the other three never happen. The spec's own probe verb requires those four, so the proposal and the probe could not both be kept, and the probe is the one with tests.

So: the tool restricts ITSELF, then starts the command as a child and waits, exactly as the darwin body waits on sandbox-exec's child. The process count is the same as darwin's -- tool plus command, no helper -- so this is NOT the "re-exec helper with a hidden flag" the spec considered and rejected; there is no second process and no hidden verb. What it costs is that the tool's own process is walled too from the moment Run restricts, which is why Run is documented below as one-way.

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
	ExitRunaway     = 137 // the tree passed a process or memory cap and was killed by its group
	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 (
	DefaultMaxProcs       = 256
	DefaultMaxMem   int64 = 8 << 30
)

The wall's caps on the tree it runs, docs/SPEC-SANDBOX.md "wall-caps-processes.w1". The tree is the process group the wrapped command leads: past either cap the whole group is killed by its group id and the run ends reporting runaway.

View Source
const Backend = "landlock"

Backend is what the SANDBOX OK line names on this platform.

View Source
const ProfileFriend = "friend"

ProfileFriend is the wall profile a nova-friend lane takes when its friend row names none (docs/SPEC-SANDBOX.md, buds-in-the-wall-r.w5).

Variables

View Source
var EgressBaseNames = []string{"github.com", "api.github.com", "objects.githubusercontent.com"}

EgressBaseNames are the three names EVERY card gets, fixed by the coordinator's page. They are named here AND asserted present in infra/image/egress.txt: the file is the contract, and this constant is checked against it rather than trusted beside it.

View Source
var LaneNetPorts = []int{443, 22}

LaneNetPorts is the network a lane's wall opens: TCP 443 (github.com, and the harness's own provider) and 22 (the bench hosts over ssh), to any host on darwin, where SBPL has no host filter, and to none on linux, where Landlock here has no port rule (Input.NetPorts).

View Source
var LaneProfiles = []string{ProfileFriend}

LaneProfiles is every wall profile a lane may name; any other name is refused.

Functions

func ABI

func ABI() string

ABI is the abi= field. On linux it is the discovered Landlock version, and "-" when there is no Landlock to have one. It is a function and not a const because this is the one platform whose answer is the machine's rather than the build's.

func ABIWith

func ABIWith(abiFn func() (int, bool)) string

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 reports whether the OS-enforced sandbox is here, on this machine. The string is what the check verb appends to its note, so it names the kernel rather than a path: there is no binary to point at, the backend is in the running kernel or it is nowhere.

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, and only when the rest of the input is sound.

func BuildEgress

func BuildEgress(in EgressInput) (EgressPlan, []Refusal)

BuildEgress resolves the allowed names, pins the addresses and renders the ruleset. It returns EVERY independent problem rather than the first (rule 16).

func CheckEgressPlan

func CheckEgressPlan(text string) (EgressAudit, []Refusal)

CheckEgressPlan parses a rendered plan back and asserts the invariants: one nova_egress table, every chain based on `policy accept`, every rule scoped to that chain's selector, every accept naming ONE address and port 443 or 53, the metadata address denied by name, and the last rule of every chain the bare selector drop. It is the test of the tests — a plan that passes this is a plan whose promises can be read off it, by a reader or by CI, without trusting the renderer that wrote it.

Anything it cannot parse is a refusal, not a shrug: an nft line this grammar does not know is a line whose effect the audit cannot judge.

func ChildEnv

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

ChildEnv keeps the caller's environment unchanged except for the temporary-directory variables it sets to the sandbox path and the agent socket variables it removes. TMPPREFIX is zsh's independent temporary-file prefix on macOS, so it must be inside the same wall even though other shells ignore it. 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 ClampedABI

func ClampedABI() (int, bool)

ClampedABI is the abi= field's companion: the ABI the wall is actually BUILT at, and whether that is below the one the kernel reports. The tool prints `used=<n>` only when the two differ, so the line on an ordinary machine is the line it has always been.

func ClampedABIWith

func ClampedABIWith(abiFn func() (int, bool)) (int, bool)

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 DeniedWrites

func DeniedWrites(home string, deny []string) []string

DeniedWrites is deny with ~/ made home.

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 EgressTableName

func EgressTableName(run string) string

EgressTableName is the nft table one run's wall lives in.

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. 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 the 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, not a platform assumption). Every caller path of this package exists, so this last answer is defence in depth.

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

func LandlockPolicyText

func LandlockPolicyText(p *Policy) (string, error)

LandlockPolicyText renders the ruleset the linux body would build from this policy, in the order addRules adds it: the read-only roots, this run's optional roots, the caller's --read and --read-noexec, then the write set. Landlock takes rules, not a profile document, so this text IS the generated policy rule 15 asks the `policy` verb to print — and like the verb, it runs nothing and applies nothing.

A path holding a control character cannot reach here: Build refuses one on every platform, so one line stays one rule. The POLICY OK line carries the counts; this body carries the sets.

func NetEnforceable

func NetEnforceable() bool

NetEnforceable reports whether --net-deny can be enforced here: TCP bind/connect arrived at ABI 4, so a kernel below it cannot enforce --net-deny and must refuse rather than pretend.

func Note

func Note() string

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

func NoteWith

func NoteWith(abiFn func() (int, bool)) string

func OKEgressRun

func OKEgressRun(s string) bool

OKEgressRun is the shape a --run may take. It is narrow because the value becomes an nft table name, which is an identifier and not a string.

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), plus the directory /var/db/xcode_select_link points at when that directory is not already a fixed root.

func ParseGPUMode

func ParseGPUMode(s string) (GPUMode, *Refusal)

ParseGPUMode accepts only the explicit capabilities. A blanket grant such as "all" is refused: filesystem, network, clipboard, and agent-socket constraints stay intact under every mode.

func PathDirectoriesWith

func PathDirectoriesWith(lookIn string, reads, writes, optRoots, homes []string) []string

PathDirectoriesWith extracts existing directories from lookIn (PATH) that are not already covered by fixed prefixes, optional roots, or the caller's reads/writes, skipping the given homes. On darwin, these directories receive file-read-metadata so that commands installed on PATH (e.g. ~/.local/bin) can be resolved and executed by name, while keeping their file contents unreadable.

func Run

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

Run applies the policy and runs the command, and returns the status the tool must exit with. A Refusal returned here is the tool saying NO before the command ran.

ONE WAY, AND SAY SO: past the restrictSelf below, THIS PROCESS is inside the wall and no call can take it back out -- a Landlock domain cannot be lifted, which is the property the whole tool rests on. A caller that runs Run twice in one process nests a second domain inside the first; that is what probeVerb does, and because every one of its walled steps uses the same policy, the nested domain is the same wall again. Anything a caller must do UNWALLED it must do before the first Run -- which is exactly why the probe verb runs write_outside_control first.

func RunawayLine

func RunawayLine(left int) string

RunawayLine is the end of the SANDBOX RUNAWAY line for what KillAndAwait answered: "killed" only when a count found nothing of the tree left.

Types

type EgressAudit

type EgressAudit struct {
	Table  string
	Chains []string
	Allow  int
	Deny   int
	Rules  int
}

EgressAudit is what CheckEgressPlan read back out of a rendered plan.

type EgressInput

type EgressInput struct {
	Run        string         // the run id; the table is nova_egress_<run>
	PolicyPath string         // named in the plan's header, for the reader
	Names      []string       // the policy file's entries, in file order
	ModelHost  string         // the ONE model host of this run; must be in Names
	Resolver   netip.Addr     // the resolver DNS is allowed to
	BenchCIDRs []netip.Prefix // the other benches, denied
	UID        string         // `meta skuid <n>` — the container's uid on the host
	Veth       string         // `iifname "<veth>"` — the container's interface
	Lookup     EgressResolver
}

EgressInput is one plan's worth of input. Every field comes from a flag or from the policy file; none of it is guessed.

type EgressPlan

type EgressPlan struct {
	Run   string                  // the run id
	Table string                  // nova_egress_<run>
	Names []string                // the names this run allows, in order
	Addrs map[string][]netip.Addr // what each name was pinned to
	Allow int                     // accept rules rendered
	Deny  int                     // drop rules rendered, the default deny included
	Text  string                  // the nft ruleset, the thing `nft -f` is handed
}

EgressPlan is the rendered ruleset and the receipt that goes with it.

type EgressResolver

type EgressResolver interface {
	LookupHost(name string) ([]netip.Addr, error)
}

EgressResolver is the whole of the plan's contact with DNS. The production body asks the bench's resolver (the same one the card is then allowed to reach); the tests put a table behind it, so a plan is built and audited without a packet.

type GPUMode

type GPUMode string

GPUMode is the explicit local GPU capability. The zero value is no GPU.

const (
	GPUNone  GPUMode = "none"
	GPUMetal GPUMode = "metal"
)

type Input

type Input struct {
	Reads []string
	// ReadsNoExec is --read-noexec: readable, recursively, and NOT EXECUTABLE. It exists
	// because a --read root carries EXECUTE on both bodies -- landlock's read subset is
	// EXECUTE|READ_FILE|READ_DIR and the darwin profile grants process-exec* globally --
	// so naming a directory the job's own user can write to (a GOPATH/bin, a
	// node_modules/.bin, a pip --user bin) lets the job RUN whatever is in it. A cache or
	// a data tree wants reading without execution, and this form makes that distinction
	// explicit so a cache cannot become executable merely because it is readable.
	ReadsNoExec []string
	Writes      []string
	Cwd         string // empty: the first --write supplies the working directory
	Tmp         string // empty: <first --write>/.nova-sandbox-tmp
	Name        string // windows container name; accepted and ignored elsewhere
	NetDeny     bool
	NetListen   bool
	NetAllow    []string // host:port the profile opens back up by name
	// NetPorts is the TCP ports a --net-deny wall opens outbound, to any host, with the
	// name resolver: a lane's wall profile (profile.go) names 443 and 22. Only with
	// NetDeny; the darwin profile grants them, and on linux they are not granted (Landlock
	// here handles no port rule), so there the denial is whole: fail closed.
	NetPorts []int
	// Deny is the paths no write of this wall may reach (a lane's wall profile: the
	// coordinator's self). Every --write, the --cwd, the --tmp and HOME is refused when it
	// is inside one or holds one, so the wall denies them by having no grant there.
	Deny        []string
	GPU         string   // --gpu none|metal; empty means none
	Argv        []string // the command and its arguments, everything after --
	Home        string   // the HOME value the child receives
	LookAt      string   // PATH to resolve the command on; empty means the process's own
	CallerHomes []string // homes to check against; empty uses callerHomes()
	MaxProcs    int      // the tree's process cap; 0 is DefaultMaxProcs
	MaxMem      int64    // the tree's resident-memory cap in bytes; 0 is DefaultMaxMem
}

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 LaneProfile

type LaneProfile struct {
	Name      string   // one of LaneProfiles; "" is ProfileFriend
	Work      string   // the friend's working directory
	Jobs      []string // her job directories outside Work
	ConfigDir string   // her CLAUDE_CONFIG_DIR, and the HOME of the wall; "" is Work
	Reads     []string // what the harness reads beyond the system roots and its own directory
	Deny      []string // the coordinator's self, never written; at least one
	Home      string   // the HOME the Deny paths starting ~/ are under
}

LaneProfile is the wall a nova-friend lane's children run inside: writes only to the friend's working directory, her job directories and her config directory (CLAUDE_CONFIG_DIR), never to a Deny path, and the network LaneNetPorts.

func (LaneProfile) Input

func (lp LaneProfile) Input(cwd string, argv []string) (Input, error)

Input is the wall's input for one command (argv) run in cwd: the profile's writes, its deny list, its network and HOME. A profile that is not a LaneProfiles name, that has no working directory, or that denies nothing, is refused.

type Policy

type Policy struct {
	Reads       []string // resolved, read-only, recursive; carries EXECUTE
	ReadsNoExec []string // resolved, read-only, recursive, and NOT executable
	Writes      []string // resolved, read+write, recursive; the first is load-bearing
	// LinkSpellings is the caller's cleaned absolute spelling of a --read, --read-noexec or
	// --write whose resolved path differs from it (rule 5): following a symlink needs read
	// on the link itself, which the resolved READn/WRITEn grants do not name.
	LinkSpellings []string
	OptRoots      []string // the platform's optional roots that EXIST on this machine
	PathDirs      []string // existing directories from PATH granted file-read-metadata
	Cwd           string
	Tmp           string
	Home          string
	Name          string
	NetDeny       bool
	NetListen     bool
	NetAllow      []string // host:port the profile opens back up by name
	NetPorts      []int    // TCP ports a --net-deny wall opens outbound (Input.NetPorts)
	Deny          []string // resolved paths no write reaches (Input.Deny)
	GPUMode       GPUMode
	MaxProcs      int      // the tree's process cap, set by Build; 0 on a hand-built policy is unbounded
	MaxMem        int64    // the tree's resident-memory cap in bytes, set by Build; 0 is unbounded
	Command       string   // the resolved absolute path of the executable
	Argv          []string // Command followed by its arguments, verbatim

	// Available is an optional seam for tests checking refusal when the backend is absent.
	// When nil, package Available() is called.
	Available func() (string, bool)

	// Tick is an optional seam for tests of the caps: the channel a count of the tree
	// waits on. When nil the tree is counted every second.
	Tick <-chan time.Time

	// LandlockABI is an optional seam for tests checking Linux Landlock ABI behavior.
	// When nil, package landlockABI is called.
	LandlockABI func() (int, bool)

	// 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 the Cwd and the Tmp, and both are recorded here as the caller's own first --write.

func (*Policy) AncestorCount

func (p *Policy) AncestorCount() int

AncestorCount is how many file-read-metadata ancestor literals the darwin profile emits for this policy: one per proper ancestor of every --read, --write, --cwd, --tmp path and optional root, "/" excluded. It is the ancestors=<n> number on the SANDBOX OK line.

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) DeleteRoots

func (p *Policy) DeleteRoots() []string

DeleteRoots is the --write roots the wall lets the command delete beneath, in the order they were given: the set the SANDBOX OK line names on deletes= (docs/SPEC-SANDBOX.md, "deletes-in-every-write-root"). It is read off DeletesIn, so the line says what the rule grants and not what a second list believes it grants.

func (*Policy) DeletesIn

func (p *Policy) DeletesIn(dir string) bool

DeletesIn reports whether the wall lets the command delete beneath dir: unlink, rmdir and rename-away. Every --write root qualifies, the data home included: what the command may create there it may remove, and a database commits by unlinking its rollback journal. The temp directory and the working directory are inside the write set, so they qualify by lying under a --write. A path outside every --write does not (docs/SPEC-SANDBOX.md, "deletes-in-every-write-root").

func (*Policy) Net

func (p *Policy) Net() string

Net is the value put 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.

func (*Policy) Over

func (p *Policy) Over(u Usage) (string, bool)

Over says whether u is past a cap, and the runaway line when it is. A cap of zero is not set: a policy Build made always carries both.

func (*Policy) Watch

func (p *Policy) Watch(tick <-chan time.Time, usage func() (Usage, error), kill func()) (stop func() string)

Watch counts the tree on every tick (every second when tick is nil) and, the first time it is past a cap, calls kill and stops. It returns stop, which ends the watch with one last count and answers the runaway line, or "" when the tree never passed a cap. The last count is for a tree whose leader exited between two ticks and left its children: a fork bomb refused by RLIMIT_NPROC ends its own shell, and the children are still the group. A count that fails is skipped: a watch that cannot look must not kill what it cannot see.

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. 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 ParseEgressPolicy

func ParseEgressPolicy(data []byte) ([]string, []Refusal)

ParseEgressPolicy reads infra/image/egress.txt: one hostname per line, `#` starts a comment, blank lines are nothing. Everything else is a refusal — the file is the reviewed contract, and a line nobody can read as a hostname is a line nobody reviewed as one.

func ResolveCallerFile

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

ResolveCallerFile validates 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

type Tree

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

Tree is the walled command's process tree, the thing the caps count and kill ("wall-caps-processes.w1"). Rule 12 keeps the tree in the CALLER's process group, so no group id of the tool's names it: it is found by parent pid, every count, from top. A member that is orphaned (its parent exits, it is reparented to pid 1) stays a member while it is still in the group it was seen in. A process orphaned between two counts was never seen and escapes the count on macOS (on linux the tool is a subreaper, so there is no such orphan); it is still in the caller's group, and the caller's group kill at its deadline is what reaches it: that is rule 12's reason.

func (*Tree) KillAndAwait

func (t *Tree) KillAndAwait() int

KillAndAwait ends the tree and answers how many of its processes are still running, bounded. A kill from one listing misses a child forked after it, which outlives its parent as an orphan never seen (the second read of 2026-10-06: 35-39 of a forking loop left running after "killed"). So the tree is FROZEN first: SIGSTOP to every member, and again on a fresh listing, until no member is new or still running; a stopped parent forks nothing and its children stay its descendants. Then SIGKILL to every member, and again until a count finds none or the bound is spent. -1 is a tree that could not be counted at the end, which is never reported as killed.

func (*Tree) Reaped

func (t *Tree) Reaped()

Reaped is called once the command's own pid has been collected: the number may be handed to an unrelated process from then on, so a tree that hung from it hangs only from the orphans it has seen. A tree that hangs from the tool keeps its top.

func (*Tree) Usage

func (t *Tree) Usage() (Usage, error)

Usage counts the tree's live processes and sums their resident bytes.

type Usage

type Usage struct {
	Procs int
	RSS   int64
}

Usage is what one count of the tree found: its live processes and their resident bytes.

func GroupUsage

func GroupUsage(pgid int) (Usage, error)

GroupUsage counts the live processes of group pgid and their resident bytes, read from /proc with no fork: a count taken while the tree is forking must not need a fork. A zombie is dead and is not counted.

Directories

Path Synopsis
Package darwincheck is the darwin profile check: it fills profiles/darwin.sb.tmpl for a scratch write set, then runs, INSIDE the wall and by ABSOLUTE path, the things a swarm worker does in its first second (cd, mkdir -p, git init, git clone --shared, a config write under HOME, cat /etc/hosts, /bin/sh -c true, killing its own child, stdout to a pipe and to a file in the write set), and asserts that a write outside, a read of the named secret, a listing of an ancestor, a connect to a socket outside the write set and a nested sandbox all FAIL, each with a control run OUTSIDE the wall so that a check cannot pass by being impossible.
Package darwincheck is the darwin profile check: it fills profiles/darwin.sb.tmpl for a scratch write set, then runs, INSIDE the wall and by ABSOLUTE path, the things a swarm worker does in its first second (cd, mkdir -p, git init, git clone --shared, a config write under HOME, cat /etc/hosts, /bin/sh -c true, killing its own child, stdout to a pipe and to a file in the write set), and asserts that a write outside, a read of the named secret, a listing of an ancestor, a connect to a socket outside the write set and a nested sandbox all FAIL, each with a control run OUTSIDE the wall so that a check cannot pass by being impossible.

Jump to

Keyboard shortcuts

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