linux

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Index

Constants

View Source
const (
	SeccompOffNR   = 0 // int    nr
	SeccompOffArch = 4 // __u32  arch

)

seccomp_data field byte offsets (see <linux/Seccomp.h>). On little-endian x86_64 the low 32 bits of a u64 arg live at the arg's base offset, so a single BPF_W|BPF_ABS load at the base offset yields the low word we compare. The args array starts at offset 16, each entry 8 bytes wide.

View Source
const CgroupScopePrefix = "lrsb-"

CgroupScopePrefix names every transient scope this backend creates. The no-dangling-cgroup Teardown test and any operator inspection key on it; the remainder of the name is crypto/rand entropy (transientScopeName).

View Source
const DNSTCPPort uint16 = 53

DNSTCPPort is the TCP port DNS-over-TCP resolution uses. Added to the net allowlist whenever policy.NetPolicy.DNS is set, alongside the RES_OPTIONS=use-vc env injection that forces glibc to use it.

View Source
const DefaultMaxPIDs int64 = 512

DefaultMaxPIDs is the pids.max applied when a policy does not set an explicit policy.Limits.MaxPIDs (SPEC §7.4). It is the load-bearing fork-bomb cap: a fork bomb grows until it hits pids.max, so ANY finite cap stops it — the value only needs to sit above real toolchain fan-out and below runaway growth.

Headroom reasoning: pids.max counts THREADS as well as processes across the whole scope, so the budget must cover every OS thread of every process a real spawn creates. A parallel build (make -jN, cargo, a Go toolchain) spawns on the order of dozens to low hundreds of concurrent tasks; 512 leaves comfortable headroom over that while remaining far below the exponential growth of a fork bomb (which reaches thousands almost immediately). Small enough to stop the bomb, large enough that a legitimate build never trips it.

View Source
const GlobScanMaxDepth = 8

GlobScanMaxDepth bounds the spawn-time glob-deny scan (SPEC §7.5). It mirrors Codex's glob_scan_max_depth precedent: deep enough to reach a repo-local `.env` a few directories down, shallow enough that the per-spawn scan of the workspace + $HOME stays cheap and bounded (a glob mask is spawn-time work on the hot path). A match below this depth is not masked for that spawn; the residual is recorded, never silently widened.

View Source
const IPProtoMPTCP = unix.IPPROTO_MPTCP

IPProtoMPTCP is IPPROTO_MPTCP (protocol 262). Named locally so the filter reads clearly; it equals unix.IPPROTO_MPTCP.

View Source
const ResOptionUseVC = "use-vc"

ResOptionUseVC is the glibc resolver option that forces DNS over TCP.

View Source
const ResOptionsEnvKey = "RES_OPTIONS"

ResOptionsEnvKey is the glibc resolver options environment variable. Setting it to include use-vc forces the stub resolver to use TCP (virtual circuit) rather than UDP — required because Rung-2 Seccomp (12b) denies UDP sockets.

View Source
const SeccompX32SyscallBit = 0x40000000

SeccompX32SyscallBit is __X32_SYSCALL_BIT. The x32 ABI shares AUDIT_ARCH_X86_64 with native x86_64 (so it passes the arch guard), but its syscall numbers are OR'd with this bit — an x32 socket() has nr = 41|0x40000000, which would NOT match SYS_socket and would fall through to ALLOW: a silent bypass of every nr-based rule. Any syscall carrying this bit is killed (fail-closed) right after the arch guard. (The arch guard alone stops i386, which uses a DIFFERENT arch value; it does NOT stop x32.)

View Source
const (
	Stage2RungOne uint8 = 1 // user+mount+pid+net namespaces + mount view + nftables
)

stage2Rung values tag the confinement tier a stage-2 spec was compiled for (SPEC §7.2). RunStage2 branches on it: Rung 1 additionally applies the bind-mount view + in-Netns nftables before the shared Landlock/Seccomp axes; a zero/unset value is treated as Rung 2 (no namespaces), the pre-Task-13 shape, so an old-shaped spec can never accidentally trigger the Rung-1 mount path.

View Source
const Stage2SentinelEnv = "LRSANDBOX_STAGE2"

Stage2SentinelEnv is the reserved environment variable that flags a re-exec'd stage-2 helper child (SPEC §6, §7.2). The Linux backend sets it on the child's environment so the child's Init() dispatches into RunStage2 before main()/ testing.M runs. It is captured OUT of the target environment before being set (see backend_linux.go), so the execve'd target never observes it — a normal harness process, where it is unset, is entirely unaffected.

View Source
const Stage2SentinelValue = "1"

Stage2SentinelValue is the single recognized sentinel value. Any other value of Stage2SentinelEnv is treated as unset (Init returns, normal path), so a stray environment variable can never make a normal process re-exec.

View Source
const Stage2SpecFD = policy.ReservedSpecFD

Stage2SpecFD is the fixed file descriptor on which the parent passes the gob-encoded Stage2Spec to the child. The parent appends the pipe's read end to cmd.ExtraFiles, and since fds 0/1/2 are stdio, ExtraFiles[0] becomes fd 3 in the child.

Variables

View Source
var ErrInitNotCalled = errors.New("sandbox: Init() was not called — call sandbox.Init() as the very first line of main() before constructing a sandboxed executor on Linux")

ErrInitNotCalled is returned while constructing an executor on Linux when a re-exec enforcement backend (Rung 1/2) would be selected but sandbox.Init() was not called first (SPEC §6). It is a leaf sentinel so consumers can errors.Is it.

Functions

func BuildRung1Ruleset

func BuildRung1Ruleset(conn *nftables.Conn, spec NftSpec) error

BuildRung1Ruleset stages the inet filter table + output chain (policy DROP) and its rules into conn (not yet flushed), in the order that keeps the metadata deny ahead of the Private accept (§5.4). It returns an error only when a CIDR fails to parse (a programming error in the fixed CIDR lists), so the stage-2 child fails closed rather than flushing a partial ruleset.

func BuildSeccompFilter

func BuildSeccompFilter() []unix.SockFilter

BuildSeccompFilter builds the Rung-2 classic-BPF program as a []unix.SockFilter. See the annotated instruction listing inline. Structure:

arch guard  -> KILL_PROCESS on mismatch (stops i386 — a different arch value)
x32 guard   -> KILL_PROCESS if nr carries __X32_SYSCALL_BIT (x32 shares the
               x86_64 arch value, so the arch guard alone does NOT stop it)
ptrace / io_uring{setup,enter,register} -> ERRNO(EACCES)
nr != socket -> ALLOW
domain != AF_INET && != AF_INET6 -> ALLOW
(type & 0xff) == SOCK_DGRAM -> ERRNO(EACCES)          (UDP)
protocol == IPPROTO_MPTCP    -> ERRNO(EACCES)          (Multipath TCP)
else -> ALLOW                                          (e.g. plain TCP)

func CIDRVerdictRule

func CIDRVerdictRule(table *nftables.Table, chain *nftables.Chain, cidr string, kind expr.VerdictKind) (*nftables.Rule, error)

CIDRVerdictRule builds "ip[6] daddr in <cidr> <verdict>" — the shared shape for the metadata DROP and Private ACCEPT rules. It guards on nfproto (the inet table carries v4+v6) before indexing the address, then masks and compares the destination address against the network.

func CgroupCompileReport

func CgroupCompileReport(cg CompiledCgroup) profile.ReportEntry

CgroupCompileReport records how the cgroup v2 resource-limit axis compiled (SPEC §7.4, §7.5). When a transient scope will be created it is Enforced; otherwise it is unenforced, distinguishing an explicit policy opt-out (policy.Limits.Disabled) from absent cgroup v2 pids delegation. Resource limits never change the isolation Level — they are containment-of-cost, not authority.

func CompileNftPlan

func CompileNftPlan(n policy.NetPolicy) compiledNftPlan

CompileNftPlan distils a policy.NetPolicy into a compiledNftPlan (SPEC §5.2, §5.4, §7.2 Rung 1). Fail-closed: an Open policy yields Confined=false (no Netns, no ruleset — the unconfined passthrough); otherwise every accept is gated by its policy.NetPolicy flag and the metadata deny is always included.

func ConfigureRung1SysProcAttr

func ConfigureRung1SysProcAttr(attr *syscall.SysProcAttr, netConfined bool)

ConfigureRung1SysProcAttr sets the Rung-1 namespace cloneflags and the uid/gid maps on the spawn's SysProcAttr (SPEC §7.2 Rung 1). The stage-2 child is clone'd into a fresh user + mount + pid namespace (+ net namespace only when egress is Confined — an OPEN policy must keep host networking, so isolating the Netns would sever it). The caller maps to root inside the user namespace so the child holds (subject to host policy) CAP_SYS_ADMIN for the mount view and CAP_NET_ADMIN for the in-Netns nftables. It EXTENDS the passed SysProcAttr (it only sets these fields), so the existing cgroup UseCgroupFD wiring and the spec pipe coexist on the same struct.

func CreateTransientCgroup

func CreateTransientCgroup(cg CompiledCgroup) (*transientCgroup, error)

CreateTransientCgroup builds this spawn's transient scope: mkdir a uniquely named child under cg.Ancestor, apply the limits (pids.max mandatory, memory/cpu optional), and open the directory fd used to join the stage-2 child at clone. It returns (nil, nil) when cg applies no limits (Enforced() == false), so the caller simply spawns without a cgroup. On any failure it tears down whatever it created and returns a *cgroupError — best-effort at the call site (§7.4), never fatal to the spawn.

func DaddrMatchExprs

func DaddrMatchExprs(ipnet *net.IPNet) []expr.Any

DaddrMatchExprs builds the "nfproto == family; daddr & mask == network" match for an IPv4 or IPv6 destination network. IPv4 daddr is 4 bytes at network-header offset 16; IPv6 daddr is 16 bytes at offset 24.

func DportAcceptRule

func DportAcceptRule(table *nftables.Table, chain *nftables.Chain, proto byte, port uint16) *nftables.Rule

DportAcceptRule builds "meta l4proto <proto> <proto> dport <port> accept". The transport-header destination port is at offset 2 length 2, matched in network byte order. Mirrors the M4 spike.

func EncodeStage2Spec

func EncodeStage2Spec(w io.Writer, spec Stage2Spec) error

EncodeStage2Spec gob-encodes a Stage2Spec to w (the parent's pipe write end). It is the symmetric counterpart to DecodeStage2Spec; the parent runs it on a goroutine so a full pipe buffer can never block the spawn. It returns the encoder error so callers can decide (the parent treats it best-effort: a failed encode leaves the child's decode to fail closed).

func EnsureResOptionsUseVC

func EnsureResOptionsUseVC(env []string) []string

EnsureResOptionsUseVC forces glibc's stub resolver onto TCP by guaranteeing RES_OPTIONS contains use-vc in the target environment (SPEC §7.2, Task 12c). UDP is Seccomp-blocked (12b), so without use-vc glibc's initial UDP query fails before it would fall back to TCP. It appends use-vc to an existing RES_OPTIONS value (space-separated, as glibc parses it) or sets a fresh one, and is a no-op when use-vc is already present. env is the target env (a fresh per-spawn copy), so mutating it in place is safe.

func FormatCPUMax

func FormatCPUMax(pct int) string

FormatCPUMax renders a MaxCPUPct as a cgroup v2 cpu.max value ("<quota> <period>", microseconds). 100% ⇒ one full core (quota == period); values above 100 permit more than one core's worth on a multi-core host.

func Ifname

func Ifname(n string) []byte

Ifname pads an interface name to the fixed 16-byte IFNAMSIZ field the nftables meta oifname match compares against (unpadded names never match). Mirrors the M4 spike.

func Init

func Init()

Init is THE re-exec dispatch entry point on Linux (SPEC §6, §7.2). Consumers call it as the very first line of main(), before any goroutine, file descriptor, or thread state is established. It inspects the reserved re-exec sentinels and dispatches exactly one of three ways:

  • Stage2SentinelEnv == Stage2SentinelValue: this process is a re-exec'd stage-2 helper. RunStage2 reads the sealed spec, (later) applies confinement, chdirs, and execve's the target; it never returns on success.
  • probeSentinelEnv set to a recognized namespace-probe mode: this is a throwaway capability-probe child. Run the one privileged op and os.Exit with its result (0 effective / non-zero denied).
  • neither set (or any unrecognized value): the normal path. Init returns immediately and is a no-op, so a stray environment variable can never make a normal process exit or exec.

Unifying both re-exec children under this single exported dispatcher (rather than a package init()) is deliberate: SPEC §6 makes Init the re-exec entry point, and a package init() that re-execs is the footgun moby/reexec avoids (it fires in every importer). One dispatcher, one place to audit.

func LandlockAccessSet

func LandlockAccessSet(access policy.FSAccess, isDir bool) landlock.AccessFSSet

LandlockAccessSet assembles the Landlock AccessFSSet for the given policy access bits and node kind. Read → read-file (+ read-dir for a directory); Exec → execute; Write → the mutation rights (matching go-landlock's own accessFSWrite set), with the directory-entry rights (make*/remove*) applied only to a directory rule. Bits absent from the policy access are never set, so a read-only entry grants no execute and no write.

func LookPathIn

func LookPathIn(name string, env []string) (string, error)

LookPathIn resolves a bare executable name against the PATH found in env (KEY=VALUE entries), returning the first entry that exists and is executable. It mirrors exec.LookPath but searches the TARGET's PATH (env) rather than the stage-2 process's own, so resolution matches the Confined environment. It does not fall back to the ambient PATH: a name that the target's PATH cannot resolve fails closed rather than resolving against the harness's environment.

func NetCompileReport

func NetCompileReport(n policy.NetPolicy, cnet CompiledNet) []profile.ReportEntry

NetCompileReport records how the Rung-2 network compilation treated each policy feature (SPEC §7.5). It is appended to the backend's CompileReport:

  • Confined: the TCP port allowlist is Enforced (network-boundary). An empty allowlist is still a boundary — it denies all TCP.
  • open egress: no restriction applied (network / unenforced) — the unconfined passthrough; the backend also withholds profile.GuaranteeNetworkBoundary.
  • Loopback/Private requested, or any egress allowed: address scoping is unenforced (address-network) — Landlock TCP rules are port-only, so Loopback/Private/metadata are not address-scopable at Rung 2 (§7.5); use Rung 1 for real address boundaries.
  • DNS: forced over TCP, glibc-dependent (Dns / narrowed).

func ParseCIDR

func ParseCIDR(s string) (*net.IPNet, error)

ParseCIDR parses a CIDR or a bare IP (treated as a host /32 or /128) into a normalized *net.IPNet. A bare IP is how the §5.4 EC2 IPv6 endpoint (fd00:ec2::254) is expressed in policy.MetadataDenyCIDRs.

func PlatformBackend

func PlatformBackend() (enforce.Backend, error)

platformBackend selects the OS enforcement backend on Linux by PROBING the host for the strongest achievable Rung (SPEC §7.2) and returning the matching backend:

  • Rung 1 → the full-isolation re-exec backend (NewBackendRung1): user+mount+pid+net namespaces + bind-mount view + in-Netns nftables, then Landlock + Seccomp + cgroup (Task 13, SPEC §7.2 Rung 1). Selected only when the probe confirmed a usable Userns+Mountns+Netns; it reports profile.LevelFull.
  • Rung 2 → the re-exec Landlock+Seccomp backend (NewBackend): FS by enumerated Landlock allowlist + Seccomp + TCP-port net, no namespaces. Sound (never wider than policy — it enforces less than Rung 1 could) and honestly reported as profile.LevelDegraded. The probe keeps the selection honest: a host that cannot enforce even Rung 2 does NOT get a re-exec backend claiming confinement.
  • Rung none → enforce.ErrUnavailable. Sandboxed execution never falls through to a direct backend.

Init() gate (fail-closed): the re-exec Linux backend requires the consumer to have called sandbox.Init() as the first line of main() (SPEC §6) — otherwise a spawned stage-2 child, not caught by Init()'s dispatch, would run the consumer's own main() instead of the confinement helper (a footgun: it would run the target UNCONFINED, or the capability probe would mis-report). When a re-exec backend is selected but Init() was not called, construction fails with ErrInitNotCalled rather than silently building an executor that cannot actually confine.

A test may still pin a backend through the unexported withBackend seam, which bypasses this selector entirely (and the package TestMain calls Init(), so the gate is satisfied for tests that do reach this path). PlatformBackend selects the strongest Linux enforcement backend this host and process can actually provide: it probes kernel capabilities, picks a Rung, and refuses to hand back a re-exec backend when Init() never ran.

func ProbeDelegatedPidsAncestor

func ProbeDelegatedPidsAncestor() string

ProbeDelegatedPidsAncestor returns the nearest Ancestor of this process's own cgroup that is BOTH writable AND distributes the pids controller to its children (so a fresh child cgroup created there immediately has a working pids.max), or "" when no such Ancestor exists. It walks up from the unified ("0::") cgroup of /proc/self/cgroup, never escaping the mount root.

func ProbeLandlockABI

func ProbeLandlockABI() int

ProbeLandlockABI returns the kernel Landlock ABI version, or 0 when Landlock is unavailable. LandlockGetABIVersion is a pure query (no ruleset created).

func ProbeSeccompFilter

func ProbeSeccompFilter() bool

ProbeSeccompFilter reports whether SECCOMP_MODE_FILTER is usable, WITHOUT installing a filter. Seccomp(SECCOMP_GET_ACTION_AVAIL, 0, &action) asks the kernel whether it recognizes a given filter return action; a zero errno means the Seccomp filter machinery is present and usable. It is unprivileged and side-effect-free. (A pre-4.14 kernel lacks GET_ACTION_AVAIL and would report false even though filter mode exists — that under-reports, which is fail-secure, and is irrelevant on the modern kernels this ships against.)

func RunStage2

func RunStage2()

RunStage2 is the stage-2 child body. It performs the setup that must succeed before the target runs and, on ANY failure, writes a short diagnostic to stderr and exits non-zero — it NEVER falls through to run the normal program (fail closed). On success it execve's the target and never returns.

func SelectBackend

func SelectBackend(r Rung, initCalled bool) (enforce.Backend, error)

SelectBackend is the pure selection logic behind platformBackend, split out so the Rung×Init-called matrix is unit-testable without touching the process globals. A re-exec Rung (1/2) requires initCalled; Rung none fails closed.

func SelfCgroupDir

func SelfCgroupDir() (string, bool)

SelfCgroupDir resolves the absolute directory of this process's own cgroup v2 node from the unified ("0::") line of /proc/self/cgroup. The relative path is cleaned and re-anchored under the mount root so a crafted cgroup path cannot escape it.

Types

type Backend

type Backend struct {
	// CgroupPids is the writable cgroup v2 Ancestor distributing the pids
	// controller, probed ONCE at construction (Task 14, SPEC §7.4), or "" when no
	// such Ancestor exists. It decides — at compile time — whether the
	// ResourceLimits guarantee holds; each spawn creates its transient scope under
	// it. Probing at construction (not per spawn) makes the guarantee a stable
	// property of the executor: availability is measured once, the same value the
	// per-spawn configure uses.
	CgroupPids string
	// Rung is the confinement tier this backend compiles for (Task 13, SPEC §7.2):
	// RungTwo (Landlock + Seccomp, no namespaces) or RungOne (namespaces + mount
	// view + nftables, then Landlock + Seccomp). compile branches on it. The zero
	// value would be RungNone, but the constructors always set a re-exec Rung, so a
	// backend selected by platformBackend is never RungNone.
	Rung Rung
}

Backend is the Linux OS-enforcement backend (SPEC §7.2). It compiles a policy into a enforce.Spec whose wrap re-execs THIS binary (/proc/self/exe) into a stage-2 helper (Init -> RunStage2) that becomes the Confined target.

Task 12a wires the RUNG-2 filesystem axis: compile distils the policy's FS entries into a policy.CompiledFS, the per-spawn wrap enumerates that into a flat Landlock allowlist against the live filesystem (snapshot semantics), and the stage-2 child applies the ruleset before execve. Rung 1 (namespaces/cgroup, Task 13), Seccomp (Task 14), and network scoping (Task 12c) still fill in later; until then the backend reports profile.LevelDegraded with the write-boundary + read-deny + env-scrub guarantees a Rung-2 FS confinement genuinely upholds.

func NewBackend

func NewBackend() *Backend

NewBackend constructs the RUNG-2 Linux backend. It keeps its no-argument signature (existing callers and tests rely on it — withBackend(NewBackend()) pins Rung 2) and probes the delegated cgroup v2 pids Ancestor here so compile can decide the ResourceLimits guarantee (SPEC §7.4). Rung-2 FS confinement is compiled from the policy alone.

func NewBackendRung1

func NewBackendRung1() *Backend

NewBackendRung1 constructs the RUNG-1 (full-isolation) Linux enforce.Backend (Task 13, SPEC §7.2 Rung 1): user+mount+pid+net namespaces via the stage-2 SysProcAttr cloneflags, a bind-mount view (restricted-read + deny-by-mask), in-Netns nftables address filtering, then Landlock + Seccomp + cgroup. It is selected by platformBackend only on a host whose probe confirmed a usable Userns+Mountns+Netns (SelectRung -> RungOne). It shares the cgroup probe with Rung 2.

func (Backend) Compile

compile dispatches on the backend's Rung (SPEC §7.2): Rung 1 compiles the full namespace/mount/nftables tier (compileRung1); Rung 2 (and the no-arg NewBackend, which existing tests pin) compiles the Landlock+Seccomp tier (compileRung2). It never errors — a policy that compiles to a narrower ruleset is reported via level/bits/report, not via err.

func (Backend) CompileWithPathHandles

func (b Backend) CompileWithPathHandles(p policy.Effective, handles []*policy.PathHandle) (enforce.Spec, profile.CompileReport, uint8, uint64, error)

type BindSpec

type BindSpec struct {
	Source   string // host path bound into the view
	Target   string // absolute path inside the new root (== Source)
	ReadOnly bool   // remount the bind read-only after binding
	IsDir    bool   // directory (bind a dir) vs regular file (bind a file)
}

BindSpec is one bind mount in the Rung-1 view, gob-encoded across the re-exec (exported concrete type). Target equals the host Source path, so the view preserves absolute paths — Landlock rules and chdir(workspace) still resolve after pivot_root. ReadOnly re-masks the bind read-only (carveouts, read roots).

type Caps

type Caps struct {
	// LandlockABI is the kernel's Landlock ABI version, or 0 when Landlock is
	// unavailable. Rung 1 needs >=1 (any FS rules); Rung 2 needs >=4 (that is
	// where Landlock TCP port rules land, which Rung 2's port allowlist uses).
	LandlockABI int
	// Seccomp reports whether SECCOMP_MODE_FILTER (classic-BPF filters) can be
	// installed — probed side-effect-free via SECCOMP_GET_ACTION_AVAIL, which
	// installs nothing. Both rungs apply a Seccomp filter in the stage-2 child.
	Seccomp bool
	// Userns reports a USABLE unprivileged user namespace: one that can be
	// created AND grants effective privilege inside it (at least one of the
	// Rung-1 capabilities — CAP_SYS_ADMIN for the mount view or CAP_NET_ADMIN
	// for the Netns). A Userns that CREATES but is stripped of effective
	// capabilities (Ubuntu's apparmor_restrict_unprivileged_userns=1) is
	// reported false, because it cannot support Rung 1. It is exactly
	// (Mountns || Netns), so the invariants "Netns implies Userns" and
	// "Mountns implies Userns" hold by construction.
	Userns bool
	// Netns reports a net namespace created together with a Userns in which
	// CAP_NET_ADMIN is EFFECTIVE (proven by bringing Loopback up). This is what
	// Rung 1 needs to run in-namespace nftables (SPEC §5.2). Implies Userns.
	Netns bool
	// Mountns reports a mount namespace created together with a Userns in which
	// CAP_SYS_ADMIN is EFFECTIVE (proven by a Private recursive remount of /,
	// Confined to the throwaway child's own mount namespace). This is what
	// Rung 1 needs for its bind-mount view. Implies Userns.
	Mountns bool
	// CgroupV2 reports the cgroup v2 unified hierarchy is mounted (used by the
	// resource-limit backend, SPEC §7.4). Not part of the Rung ladder.
	CgroupV2 bool
	// CgroupPids is the nearest writable cgroup Ancestor that distributes the
	// pids controller to its children, or "" when none exists. A non-empty
	// value implies CgroupV2. Not part of the Rung ladder.
	CgroupPids string
}

Caps is the runtime capability snapshot the Linux backend selector reads while constructing an executor to pick the strongest achievable enforcement Rung (SPEC §7.2). Every field is a MEASURED fact about THIS host, taken by an active probe (not assumed): a mechanism that cannot be confirmed is reported absent (false / 0 / ""), which is the fail-secure default — an over-reported capability would let the selector claim a Rung the kernel will not enforce.

func ProbeCaps

func ProbeCaps() Caps

ProbeCaps actively measures every capability the Rung ladder depends on. It has NO lasting side effects on the calling process: the namespace probes run in throwaway forked children (see probeNamespaceCap), the Seccomp probe only queries availability, and the Landlock/cgroup probes are read-only.

func (Caps) SelectRung

func (c Caps) SelectRung() Rung

SelectRung picks the strongest achievable Rung from a capability snapshot, per the SPEC §7.2 ladder. Rung 1 requires the three namespaces plus Landlock (any ABI >= 1, since it scopes network with nftables rather than Landlock TCP rules) plus Seccomp. Rung 2 requires Landlock ABI >= 4 (TCP port rules) plus Seccomp. Anything less is RungNone. It is fail-secure: a missing capability can only ever LOWER the Rung.

type CgroupProofError

type CgroupProofError struct {
	Op  string
	Err error
}

CgroupProofError is a typed, retryable lifetime-containment proof failure (SPEC Task 12b): a cgroup.kill error, a cgroup.procs read/open error, a context timeout while waiting for the scope to drain, or a removal error. Op names the failing phase; Err wraps the underlying cause for errors.Is/errors.As. It never fires for the pre-existing best-effort resource-limit cgroup path (Teardown), which stays void/best-effort by design.

func (*CgroupProofError) Error

func (e *CgroupProofError) Error() string

func (*CgroupProofError) Unwrap

func (e *CgroupProofError) Unwrap() error

type CompiledCgroup

type CompiledCgroup struct {
	// Ancestor is the writable cgroup v2 node distributing the pids controller
	// under which each spawn's transient scope is created. "" ⇒ apply no limits.
	Ancestor string
	// PidsMax is the mandatory fork-bomb cap written to pids.max. It is > 0
	// whenever Ancestor is non-empty.
	PidsMax int64
	// MemMax is memory.max in bytes; 0 ⇒ do not set (optional cost limit).
	MemMax int64
	// CPUPct is cpu.max as a percentage of one core; 0 ⇒ do not set (optional).
	CPUPct int
	// Disabled records an explicit policy.Limits.Disabled opt-out (vs delegation absent)
	// so the compile report can distinguish the two when Ancestor is "".
	Disabled bool
}

CompiledCgroup is the resolved, per-executor cgroup v2 resource-limit plan (SPEC §7.4). It is compiled once from the policy's policy.Limits plus the backend's probed delegated pids Ancestor, then consumed by each spawn's configure to build a transient scope. An empty Ancestor means NO limits are applied (delegation unavailable or policy.Limits.Disabled) — the fail-secure default.

func CompileCgroupPolicy

func CompileCgroupPolicy(l policy.Limits, Ancestor string) CompiledCgroup

CompileCgroupPolicy resolves a policy.Limits policy against the probed delegated pids Ancestor into a CompiledCgroup (SPEC §7.4). Fail-secure: an empty Ancestor (no cgroup v2 pids delegation) or policy.Limits.Disabled yields a plan that applies NO limits (Enforced() == false). Otherwise pids.max is the mandatory cap (policy.Limits.MaxPIDs when > 0, else DefaultMaxPIDs); memory.max and cpu.max are carried only when explicitly set (> 0).

func (CompiledCgroup) Enforced

func (c CompiledCgroup) Enforced() bool

Enforced reports whether this plan will create a transient scope (and thus whether the ResourceLimits guarantee holds). It is the single fail-secure gate: no Ancestor ⇒ no limits ⇒ no guarantee.

type CompiledNet

type CompiledNet struct {
	// Confined reports whether the stage-2 child applies RestrictNet at all. It is
	// true whenever the policy does NOT grant open egress (!policy.NetPolicy.Open); false
	// leaves TCP unrestricted (the unconfined/trusted-with-open passthrough).
	Confined bool
	// TcpPorts are the TCP ports ConnectTCP is granted for. Empty (with Confined)
	// means the allowlist is empty and ALL TCP connect is denied — the fail-closed
	// direction (never wider than policy).
	TcpPorts []uint16
	// Dns reports whether DNS-over-TCP forcing is requested (port 53 already folded
	// into TcpPorts; this drives the RES_OPTIONS=use-vc target-env injection).
	Dns bool
}

CompiledNet is the Rung-2 network intent distilled from a policy.NetPolicy at compile time: whether to apply a Landlock net restriction at all, the TCP ports the target may connect to, and whether DNS-over-TCP env forcing is requested. It crosses no boundary itself (the wrap closure closes over it); its fields flow into the gob-encoded Stage2Spec (NetConfined/NetTCPPorts) and the target env.

func CompileNetPolicy

func CompileNetPolicy(n policy.NetPolicy) CompiledNet

CompileNetPolicy distils a policy.NetPolicy into a CompiledNet (SPEC §5.2, §7.2). The mapping is deliberately fail-closed — never WIDER than the policy:

  • Open egress (policy.NetPolicy.Open): Confined=false. The stage-2 child does NOT call RestrictNet, leaving TCP unrestricted. This is the unconfined case (Open is set only by an explicitly acknowledged unconfined profile), so the backend does not claim a network boundary for it.
  • Otherwise: Confined=true. The TCP allowlist is policy.NetPolicy.Ports, plus port 53 when policy.NetPolicy.DNS (DNS over TCP). An empty result denies all TCP — a completely blocked posture. Loopback/Private are NOT foldable into a port allowlist (they are address predicates), so they do not widen the ports; they are recorded unenforced by NetCompileReport.

type LifetimeScope

type LifetimeScope interface {
	Join(attr *syscall.SysProcAttr)
	KillAndWait(ctx context.Context) error
}

LifetimeScope is a delegated cgroup v2 scope created purely for exact process-tree containment (SPEC Task 12b) — independent of, and never conflated with, any policy.Limits resource-limit configuration. Join wires it onto a spawn's SysProcAttr before Start; KillAndWait is the mandatory, result-bearing zero-proof a supervised spawn's confirmed teardown depends on. It is the Rung-2 counterpart to Rung 1's PID-namespace containment (which needs no cgroup at all: the kernel's own namespace-teardown-on-init- exit guarantee is exact by construction).

func NewLifetimeScope

func NewLifetimeScope(ancestor string) (LifetimeScope, error)

NewLifetimeScope creates one supervised spawn's dedicated lifetime cgroup under ancestor — the backend's already-probed delegated pids Ancestor (Backend.CgroupPids). It applies only the load-bearing pids.max safety cap (DefaultMaxPIDs), never a caller-tunable resource limit: this scope's sole purpose is an exact cgroup.kill + cgroup.procs-empty containment proof, not cost control (the separate, policy-driven, best-effort resource-limit cgroup is CompiledCgroup/CreateTransientCgroup, unchanged by this function). ancestor == "" (no delegation) fails closed with enforce.ErrLifetimeContainmentUnavailable — there is no best-effort fallback for a supervised Rung-2 spawn's containment (SPEC Task 12b).

type MaskSpec

type MaskSpec struct {
	Target string // absolute path to mask (host path)
	IsDir  bool   // empty tmpfs (dir) vs empty file bind (file)
}

MaskSpec is an empty read-only mask over a path (a fixed-path secret deny or a glob-deny match), gob-encoded. It hides the real path's contents behind an empty dir/file so a deny beats any covering allow (§7.5). Applied after every bind so the mask always wins.

func ScanGlobDenies

func ScanGlobDenies(roots, globs []string, maxDepth int) []MaskSpec

ScanGlobDenies bounded-walks each root to maxDepth, masking every entry whose BASENAME matches a glob-deny pattern (SPEC §7.5). A pattern like **/.env* is reduced to its final segment (.env*) and matched against each entry name at any depth. Symlinks are never followed (fail secure — do not chase a link out of the scanned tree). Matches are de-duplicated across roots. This is a filesystem walk only (no namespaces), so it runs on every host.

type MountViewPlan

type MountViewPlan struct {
	// RWBinds are the writable allow roots (policy.WriteAccess) — bound rw into the view.
	RWBinds []string
	// ROBinds are the read-only allow roots (policy.ReadAccess, no policy.WriteAccess) — bound
	// ro. Carveouts (a policy.ReadAccess allow nested under a writable root, e.g. .git)
	// are ordinary ROBinds; nesting is resolved by applying binds parents-first so
	// the ro carveout re-masks the rw root it sits under.
	ROBinds []string
	// DenyMasks are literal (non-glob) fixed-path denies that have no
	// higher-precedence restoration and can therefore use a coarse empty
	// read-only mask. Restored literal precedence composes through Landlock.
	DenyMasks []string
	// GlobDenies are the glob deny patterns (e.g. **/.env*), Enforced by spawn-time
	// bounded enumeration (ScanGlobDenies) into empty read-only masks.
	GlobDenies []string
	// contains filtered or unexported fields
}

MountViewPlan is the Rung-1 filesystem intent distilled from a policy.Effective at compile time (SPEC §7.2 Rung 1, §7.5). It holds bind ROOTS and deny intent, not stat'd entries: the dir/file classification and the glob scan are redone per spawn (EnumerateMountView) so the view is a fresh snapshot each time.

func CompileMountView

func CompileMountView(p policy.Effective) MountViewPlan

CompileMountView distils a policy.Effective's FS entries into a MountViewPlan (SPEC §7.2 Rung 1). Literal allow roots become rw or ro binds; literal denies become masks when they deny every axis and have no narrower restoration; glob denies are carried for the spawn-time scan. Allow globs are dropped — a mount cannot express a glob allow, and dropping under-grants (fail secure). Landlock is layered over the mount view to enforce partial-axis and restored descendants.

type MountViewSpec

type MountViewSpec struct {
	Binds []BindSpec
	Masks []MaskSpec
}

MountViewSpec is the fully enumerated Rung-1 bind-mount view for one spawn, gob-encoded into the Stage2Spec (SPEC §7.2 Rung 1, §7.5). Binds are ordered parents-first (EnumerateMountView sorts them) so a nested ro carveout re-masks the rw root it sits under; Masks are applied after all Binds so a deny wins.

func EnumerateMountView

func EnumerateMountView(plan MountViewPlan) (MountViewSpec, error)

EnumerateMountView turns a compile-time MountViewPlan into a spawn-time MountViewSpec: it stats each bind root (classifying dir vs file), sorts the binds parents-first so nesting re-masks correctly, and re-runs the glob scan for a fresh mask snapshot. An absent protected child under an active writable bind is an error: silently dropping its ro bind or mask would let the target create and reach it after launch. It walks the live filesystem but touches no namespaces, so it runs on every host and is unit-testable.

type NftSpec

type NftSpec struct {
	Confined      bool
	TCPPorts      []uint16
	Loopback      bool
	Private       bool
	DNS           bool
	MetadataCIDRs []string
}

NftSpec is the gob-encoded Rung-1 nftables plan carried on the Stage2Spec (exported concrete type). Confined=false means no ruleset is installed.

type Rung

type Rung uint8

Rung is the OS-enforcement tier a Linux host can achieve (SPEC §7.2), strongest last so the numeric order matches strength.

const (
	// RungNone means no usable OS enforcement mechanism -> profile.LevelNone.
	RungNone Rung = iota
	// RungTwo means Landlock (v4+) + Seccomp with NO namespaces: FS by
	// enumerated allowlist and a TCP port allowlist, no address scoping.
	RungTwo
	// RungOne means namespaces (user+mount+net) + Landlock + Seccomp: the full
	// ladder, including in-Netns nftables address scoping and the mount view.
	RungOne
)

type Stage2Error

type Stage2Error struct {
	Op  string // the failing step, e.g. "decode spec", "chdir", "exec"
	Err error  // the wrapped underlying error
}

Stage2Error is a typed, fail-closed stage-2 setup failure (SPEC §7.2). Every step before the final execve — opening the spec fd, decoding the spec, chdir — returns one of these on failure so RunStage2 can never fall through to run the normal program; it wraps the underlying cause for errors.As/Unwrap.

func (*Stage2Error) Error

func (e *Stage2Error) Error() string

func (*Stage2Error) Unwrap

func (e *Stage2Error) Unwrap() error

type Stage2Spec

type Stage2Spec struct {
	Dir  string   // working directory to chdir into before exec
	Argv []string // the target argv to execve (already shell-normalized by the executor)
	Env  []string // the scrubbed child environment the TARGET should see (KEY=VALUE)
	// FSRules is the compiled, spawn-time-enumerated Landlock FS allowlist (Task
	// 12a, SPEC §7.2 Rung 2). The parent enumerates the policy's FS axis against
	// the live filesystem at spawn (policy.EnumerateFSRules) and the stage-2 child
	// rebuilds a go-landlock ruleset from it (applyLandlockRules) and restricts
	// itself before chdir/execve. policy.FSRule is a concrete, gob-encodable type.
	FSRules []policy.FSRule
	// Seccomp requests the Rung-2 Seccomp-BPF filter (Task 12b, SPEC §7.2). When
	// true the stage-2 child installs BuildSeccompFilter() AFTER Landlock and
	// BEFORE chdir/execve (installSeccompFilter), so the target inherits it across
	// the execve and dangerous syscalls (UDP/MPTCP sockets, ptrace, io_uring) are
	// soft-denied (EACCES). A bool is gob-encodable; the Rung-2 backend sets it.
	Seccomp bool
	// NetConfined requests the Rung-2 Landlock TCP-port allowlist (Task 12c, SPEC
	// §7.2, §5.2). When true the stage-2 child calls applyLandlockNet(NetTCPPorts)
	// AFTER Seccomp and BEFORE chdir/execve, confining TCP connect to NetTCPPorts
	// (and denying all other TCP) — inherited across the execve. It is false only
	// for open/unconfined egress (policy.NetPolicy.Open), where TCP is left unrestricted.
	NetConfined bool
	// NetTCPPorts are the TCP ports the target may connect to (Task 12c). An empty
	// slice with NetConfined denies ALL TCP connect.
	// []uint16 is gob-encodable; the Rung-2 backend fills it from the policy.NetPolicy.
	NetTCPPorts []uint16
	// Rung tags the confinement tier (Task 13, SPEC §7.2): Stage2RungOne applies
	// the namespaces + mount view + nftables below; stage2RungTwo (or the zero
	// value) is the no-namespace Rung-2 path. A uint8 is gob-encodable.
	Rung uint8
	// MountView is the Rung-1 bind-mount view (Task 13a/b, SPEC §7.2 Rung 1, §7.5):
	// rw/ro/ro-remask binds plus empty-mask targets (fixed denies + glob matches),
	// enumerated at spawn and applied by the stage-2 child (applyMountView) BEFORE
	// Landlock. Empty for a Rung-2 spawn. MountViewSpec is a concrete gob type.
	MountView MountViewSpec
	// NftRules is the Rung-1 in-Netns nftables plan (Task 13c, SPEC §5.2, §5.4):
	// address-scoped egress with the metadata hard-deny, installed by the stage-2
	// child (applyNftRules) inside the Netns BEFORE Landlock. NftSpec.Confined is
	// false for open egress. Empty for a Rung-2 spawn. NftSpec is a concrete gob type.
	NftRules NftSpec
	// GrantFDs are inherited O_PATH descriptors, numbered after the sealed-spec
	// fd. Landlock consumes them directly; Rung 1 may also bind from their
	// /proc/self/fd entries. Stage 2 closes them after filesystem confinement is
	// installed and before the target execs.
	GrantFDs []int
}

Stage2Spec is the sealed spawn description the parent hands the stage-2 child over a Private pipe (SPEC §7.2). It carries exactly what the child needs to become the Confined target: the working directory to chdir into, the target argv to execve, and the already-scrubbed environment the target should see.

Confinement extension point (Tasks 12/13/14): the FS rules (Landlock ruleset), network scoping (Netns/nftables), Seccomp filter parameters, and cgroup path are added here as additional fields. RunStage2 applies them (below) after decoding this spec and BEFORE chdir/execve. Keep new fields gob-encodable (exported, concrete types) since this crosses the re-exec via encoding/gob.

func DecodeStage2Spec

func DecodeStage2Spec(f *os.File) (Stage2Spec, error)

DecodeStage2Spec gob-decodes a Stage2Spec from the spec pipe. A decode failure (truncated pipe, garbage, bad fd) returns a typed Stage2Error so the caller fails closed. It is a small seam so the codec is unit-testable without a real re-exec.

Jump to

Keyboard shortcuts

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