Documentation
¶
Overview ¶
Package containment implements Governator's authority-derived host containment policy (Sol v7 RB2): the isolation requirement for a local run is derived from what the run can actually do — write, execute, produce, validate, use credentials, or forbid network — never from the operator's risk_class label. A risk_class: high contract must not silently resolve to local execution. Qualifying containment is hardened Docker (contracts.DockerRunnerConfig.IsHardened), a backend with a verified native sandbox capability, or an explicitly signed operator override. Every other case fails closed before launch.
Session 2 (Sol redteam v4, P0-4): the descendant-owning containment primitive. SysProcAttr{Setpgid: true} + Kill(-pid) only covers a process's own POSIX process group, and only on the ctx-cancellation path -- a well-behaved-looking backend that forks a setsid'd or double-forked child escapes the group entirely, and a normal (non-timeout) exit never proves the tree is actually dead. Scope replaces that assumption with a real kernel boundary: cgroup v2 membership (systemd transient scope, or a directly managed cgroup) survives setsid/double-fork/nohup, and a PID namespace makes escape structurally impossible rather than merely unlikely. Extinguish is the DESCENDANTS_TERMINATED lifecycle stage: freeze, kill, wait for kernel-confirmed extinction, then independently verify no process anywhere still holds a workspace file handle. See runtime.go's runOnce for where this runs -- always before S1's final-state fingerprint capture, never skipped, never treated as best-effort.
Index ¶
- Constants
- Variables
- func DevelopmentContainmentMode(mode string) bool
- func Effectful(c contracts.Contract) bool
- func Enforce(c contracts.Contract, externallyEnforced bool, pubKeyHex string) error
- func EnforcePolicy(c contracts.Contract, externallyEnforced bool, pubKeyHex string, ...) error
- func LocalEffectfulTieringEnforced(mode string) bool
- func NetworkForbidden(c contracts.Contract) bool
- func OverrideMessage(jobID, contractHash, reason string) []byte
- func RequiresHostContainment(c contracts.Contract, enforceLocalEffectful bool) bool
- func RequiresStrongDescendantContainment(c contracts.Contract, enforceLocalEffectful bool) bool
- func SigningMessage(c contracts.Contract) ([]byte, error)
- func VerifyOverride(c contracts.Contract, pubKeyHex string) bool
- func WithEnvironment(ctx context.Context, env ContainmentEnvironment) context.Context
- func WithScope(ctx context.Context, s *Scope) context.Context
- type CgroupCapabilities
- type ContainmentEnvironment
- type Proof
- type Scope
- func (s *Scope) Command(ctx context.Context, bin string, args []string, dir string) *exec.Cmd
- func (s *Scope) CommandWith(ctx context.Context, alloc *toolregistry.FDAllocator, bin string, ...) *exec.Cmd
- func (s *Scope) Extinguish(ctx context.Context, deadline time.Duration, workspacePath string) (Proof, error)
- func (s *Scope) IsStrong() bool
- func (s *Scope) Method() ScopeMethod
- func (s *Scope) RunID() string
- func (s *Scope) Started(pid int)
- type ScopeMethod
Constants ¶
const DefaultExtinctionDeadline = 5 * time.Second
DefaultExtinctionDeadline bounds how long Extinguish waits for the kernel to confirm every process inside a Scope is dead before hard-failing the run. There is no unbounded wait path -- report P0-4 is explicit that "process exited, therefore done" is exactly the assumption being removed.
Variables ¶
var ExtinguishGateForTesting func() error
ExtinguishGateForTesting is a TEST-ONLY synchronization seam (Sol12 rc5 Session 2, P0-1). When non-nil, Scope.Extinguish invokes it and blocks until it returns BEFORE beginning any kill/freeze logic, so a red-team fixture can PROVE a descendant reached an intended state (e.g. an uninterruptible blocking read) BEFORE Governator begins extinction. This replaces the timing assumption at the heart of TestV7Case8's original flake (report case 8: "the timing fixture did not enter its expected blocking-read state before its deadline"). If the gate returns a non-nil error, Extinguish returns that error without killing -- the fixture uses this to surface a genuine host-capability failure (a kernel that kills FUSE-blocked readers) as a deterministic, reasoned refusal rather than a post-hoc timeout skip. Production code MUST NEVER set this; only _test.go / redteam-corpus code assigns it (and defers restoring nil), mirroring internal/enforce.SelfExeOverride.
var ForceDegradedScopeForTesting atomic.Bool
ForceDegradedScopeForTesting is a TEST-ONLY seam. When true, NewScope returns a degraded (bare process-group) scope without probing for any real descendant-owning primitive, so the red-team corpus can exercise Governator's approval/merge/replay paths end to end on hosts that lack systemd --user, a usable cgroup v2 subtree, or a PID namespace.
It exists ONLY because the Sol11 P0-3 defect it replaces — the GOV_CONTAINMENT_FORCE_DEGRADED environment variable — was a production bypass: any launcher, wrapper or compromised shell could export that var to force degraded containment for a stage that should have failed closed. An inherited environment variable must never weaken production authority (Sol11 P0-3 / the rc5 governing invariant); a package-level Go variable set only by _test.go code cannot be flipped by environment, so it is the sanctioned test substitute. Production code MUST NEVER set this; nothing links the setter into a release binary's execution path. Mirrors the established test-seam pattern of internal/enforce.SelfExeOverride and ForceUnsupported.
ScopeSelectionForceUnavailableForTesting is a TEST-ONLY seam (Sol12 rc5 Session 2, P0-1). When set, newSystemdUserScope returns a deterministic "systemd user manager unavailable" error immediately after the nil-handle check -- WITHOUT touching /run/systemd/system, the live user bus, or the systemd-run probe -- so the scope-selection FAILURE path (and its descriptor-leak invariant) can be exercised on every host, including one that genuinely has a live systemd --user manager. Before this seam, TestV10Case12 (report case 12) could only run where the host truly lacked systemd --user, making it mutually exclusive with TestV10Case13 (the real live-systemd acceptance test) on any single host -- so a correct single-host zero-skip red-team run was structurally impossible. Production code MUST NEVER set this; only _test.go code in this package and the redteam corpus (behind the redteam build tag) flips it, exactly like ForceDegradedScopeForTesting. The forced error fires at the same logical point a genuinely-absent user bus would (after the borrowed handle is confirmed non-nil, before any probe), so the borrow/ownership invariant under test is the real one.
var UnitMaterializationForceUnobservedForTesting atomic.Bool
UnitMaterializationForceUnobservedForTesting is a TEST-ONLY seam (Sol14 rc7 Session 9a, P1-2). When set, resolveCgroupFromPID reports the SAME "generated systemd unit was never confirmed" resolve error the deadline loop produces when a transient scope registers with systemd but its cgroup never materializes for the launched pid -- immediately, without waiting out the 2-second deadline and without depending on this host having (or lacking) a live systemd --user manager.
Before this seam, TestV6Case28SystemdUnitNeverMaterializingFailsClosed could only assert its property on a host with NO systemd --user manager, because forcing "unit registers but is never observed within deadline" on a host that genuinely has systemd requires adversarial control over systemd the red-team package cannot exercise. It therefore skipped on every systemd host and was carried as an OPEN GAP exclusion -- "excluded but happened to pass" is not durable release policy (Sol14 P1-2). With the seam the case is deterministic on both host classes: the run must fail closed rather than reach APPROVED backed by a scope identity it never actually confirmed.
The forced error fires at the same logical point, with the same message and the same resolveErr field, that a genuinely non-materializing unit produces, so the fail-closed invariant under test is the real one -- only the trigger is deterministic. Production code MUST NEVER set this; only _test.go code and the redteam corpus (behind the redteam build tag) flip it, exactly like ScopeSelectionForceUnavailableForTesting above.
Functions ¶
func DevelopmentContainmentMode ¶
DevelopmentContainmentMode reports whether the run is executing under the development-only local_effectful_tiering: "off" compatibility mode (Sol11 P0-4). When true, effectful local work may proceed without host containment, but the transaction is NON-APPROVING by construction: strict replay is disabled, merge is disabled, and the final result can never reach APPROVED. Any production approval exception must instead use the signed containment override flow (VerifyOverride). This is the inverse of LocalEffectfulTieringEnforced, exposed as a named predicate so every approval/merge/replay gate asserts the same development-mode signal without re-spelling the literal "off" comparison.
func Effectful ¶
Effectful reports whether a contract can create persistent effects or run external helper/controller stages. Pure read-only scout contracts with no declared external stages can avoid host containment; any write, produced artifact, validator, cleanup validator, credential mount, network-deny requirement, or helper-launch authority is effectful for containment.
func Enforce ¶
Enforce applies the default risk-class containment policy. It preserves the historical call shape while enabling Session 6's enforced-by-default medium/high effectful local-run gate.
func EnforcePolicy ¶
func EnforcePolicy(c contracts.Contract, externallyEnforced bool, pubKeyHex string, enforceLocalEffectful bool) error
EnforcePolicy applies the authority-derived containment policy and returns a non-nil error when a contract lacks qualifying containment. It is fail-closed by construction: pubKeyHex is the operator override public key from config — empty means overrides are refused.
externallyEnforced reports whether Governator's OWN external-enforcement layer is available for a "local" runner (internal/enforce: Landlock LSM filesystem confinement + a network namespace with no route, applied to the launched process from outside it, independent of anything the backend claims about itself). Per Sol P0-3 (Session 5, report §9 attack 5): a backend's declared or probe-attested native sandbox is evidence, never proof — a program that knows it is being tested can behave only during the test. It no longer qualifies a "local" runner on its own; only this externally-enforced boundary or a signed operator override does.
func LocalEffectfulTieringEnforced ¶
LocalEffectfulTieringEnforced reports whether Session 6's local-run gate is active. Unknown/empty values fail closed to enforced; config validation keeps operator-authored values to "enforce" or "off".
func NetworkForbidden ¶
NetworkForbidden reports whether the contract's effective network permission is deny. Today SpecFromContract defaults to no-network and forbidden.behaviors may also state it explicitly; either way, S1 requires an externally enforced no-network boundary rather than a transcript check.
func OverrideMessage ¶
OverrideMessage is the exact bytes an operator signs (ed25519) to authorize a containment override for one contract. Binding the job_id prevents an override minted for one high-risk job being replayed against another; binding contractHash (the contract's hash with its containment block cleared) prevents the sharper replay where the SAME job's contract body is edited after signing — widened scope, network enablement, a different image — while the old signature keeps verifying. Any content edit changes the hash and invalidates the signature. Exposed so signing tooling and tests produce matching signatures without duplicating the format.
func RequiresHostContainment ¶
RequiresHostContainment reports whether this contract must prove host-level containment. Selection is authority-derived, never risk-label-derived (Sol v7 RB2): risk_class is a policy description, not a security boundary, so a malicious or mistaken contract author cannot escape containment by labelling a highly-capable operation low/unlabeled.
The authority-derived baseline is scoped to local execution. A Docker runner's container is its host boundary; risk_class may still strengthen Docker requirements, but a low label can never weaken a local run's baseline.
func RequiresStrongDescendantContainment ¶
RequiresStrongDescendantContainment reports whether a launch must refuse a degraded process-group fallback. It is intentionally authority-based rather than risk-label-based: any effectful job or high-risk job needs a real descendant-owning primitive.
func SigningMessage ¶
SigningMessage builds the exact bytes an operator signs to authorize a containment override for contract c: OverrideMessage over c's job_id, the hash of c with its containment block cleared (the signature can't cover itself), and the override reason. Exposed so `gov containment message`, tests, and VerifyOverride can never drift on the format.
func VerifyOverride ¶
VerifyOverride reports whether the contract carries a valid signed override for its job_id AND its exact content, verified against pubKeyHex. An empty pubKeyHex refuses every override (fail-closed: no operator key configured means no escape hatch). The signed message is SigningMessage(c) — see OverrideMessage for why the contract hash is part of what's signed.
func WithEnvironment ¶
func WithEnvironment(ctx context.Context, env ContainmentEnvironment) context.Context
WithEnvironment attaches the run's frozen ContainmentEnvironment to ctx (rc4 Session 2, Sol10 P0-2), mirroring WithScope, so every NewScope call site for the run's whole lifetime -- the run-level Scope (constructed where WithEnvironment is called) and every stage's own Scope (internal/stage.Executor.Run, several packages and call layers away) -- retrieves the SAME resolved handles via EnvironmentFromContext rather than each independently resolving the trusted-tool registry.
Types ¶
type CgroupCapabilities ¶
CgroupCapabilities is a descriptive snapshot of this process's cgroup v2 direct-management capability, folded into ContainmentEnvironmentHash (internal/runtime/identity.go) so a host's cgroup capability is part of the run's replay identity like everything else ContainmentEnvironment describes. Unlike SystemdRun/Unshare, newDirectCgroupScope does NOT consume this to decide whether to attempt cgroup-direct: cgroup-direct launches the caller's own already-verified bin directly (never a "primitive binary" needing registry trust the way systemd-run/unshare do, see Command's ScopeCgroupDirect branch), so it carries none of P0-2's TOCTOU concern, and several legitimate callers (internal/assay.Evaluate, notably) construct a Scope via a bare context.Context that was never threaded through containment.WithEnvironment -- requiring a resolved CgroupCapabilities there would wrongly disable the strongest containment method available on hosts with a perfectly usable cgroup v2 hierarchy. newDirectCgroupScope always probes live, exactly as it did before this type existed.
type ContainmentEnvironment ¶
type ContainmentEnvironment struct {
SystemdRun *toolregistry.Handle
Cgroup CgroupCapabilities
}
ContainmentEnvironment is the frozen set of descendant-owning containment primitives a whole run's replay identity is evaluated against (rc4 Session 2, Sol10 P0-2). Before this type existed, NewScope called toolregistry.Load() and ResolveHandle fresh on every invocation -- once for the run-level Scope, again for every stage's own Scope (internal/stage. Executor.Run) -- so the trusted-tool registry could be reloaded, and in principle observe different enrolled state, after the run's environment and replay identity were already frozen (buildRunEnvironment, called exactly once at the top of runOnce). ResolveEnvironment now does that resolution exactly once, from the SAME frozen registry every other trust decision in the run uses, and every NewScope call for the run's whole lifetime -- backend and every stage -- is handed this one value rather than resolving its own.
SystemdRun/Unshare are nil when that primitive is not enrolled/resolvable on this host; NewScope's existing fallback chain treats a nil handle exactly like the old resolution failure it replaces.
func EnvironmentFromContext ¶
func EnvironmentFromContext(ctx context.Context) (ContainmentEnvironment, bool)
EnvironmentFromContext retrieves a ContainmentEnvironment attached by WithEnvironment. ok is false only for a launch that never went through a governed runtime.Runner (doctor probes, direct package tests); NewScope treats the resulting zero value exactly like every primitive being unresolvable, falling back through its normal chain.
func ResolveEnvironment ¶
func ResolveEnvironment(registry *toolregistry.Registry) (ContainmentEnvironment, error)
ResolveEnvironment resolves every containment primitive's held handle exactly once from registry -- the caller's already-frozen trusted-tool registry (internal/runtime.RunEnvironment.ToolRegistry), never a fresh toolregistry.Load(). A primitive that is not enrolled or fails registry-verification simply resolves to a nil handle; it is not a hard error here, mirroring NewScope's pre-existing "try the next weaker primitive" fallback discipline for the whole run rather than per attempt.
func (ContainmentEnvironment) Close ¶
func (e ContainmentEnvironment) Close() error
Close releases every handle this environment holds. The caller that resolved the environment (ResolveEnvironment, called exactly once before replay) owns this and must call it exactly once, after every Scope built from this environment across the run's entire lifetime -- backend and every stage -- has finished. Individual Scopes never close these handles; they only borrow them for the duration of one launch (see Scope.Command).
type Proof ¶
type Proof struct {
Method ScopeMethod `json:"method"`
Frozen bool `json:"frozen"`
Killed bool `json:"killed"`
Waited time.Duration `json:"waited_ns"`
WorkspaceFDScanClean bool `json:"workspace_fd_scan_clean"`
Degraded bool `json:"degraded,omitempty"`
Note string `json:"note,omitempty"`
// ProcessesObservedPeak (Sol P0-3/P1-15 effect ledger) is the number of
// PIDs cgroup.procs listed for this scope at the moment it was frozen --
// i.e. every process this launch ever spawned that was still alive right
// before extinction began, read from the kernel's own accounting, not
// from anything the backend reported about its own descendants. -1 when
// the scope method has no cgroup to read (PID namespace, degraded).
ProcessesObservedPeak int `json:"processes_observed_peak"`
}
Proof is the DESCENDANTS_TERMINATED stage's recorded evidence -- exactly what was frozen, killed, and independently confirmed dead before the run was allowed to proceed to S1's final-state fingerprint/tree capture. Approval is impossible without one of these attached to the run record.
type Scope ¶
type Scope struct {
// contains filtered or unexported fields
}
Scope owns the entire descendant tree spawned by one governed subprocess launch. Exactly one is constructed per run, before the backend is started, and threaded through to the launch site via WithScope/ScopeFromContext so every backend adapter's shared runCLI path (agents.defaultExecutor, runner.LocalWorktreeRunner.executor) launches inside it without each adapter needing to know how.
func NewScope ¶
func NewScope(runID string, requireStrong bool, env ContainmentEnvironment) (*Scope, error)
NewScope selects the strongest descendant-owning primitive available on this host, in the order the plan specifies: systemd --user transient scope, then a directly managed cgroup v2 subtree, then a PID namespace. requireStrong callers refuse outright when none qualifies rather than silently falling back to a bare process group.
env is the run's frozen ContainmentEnvironment (rc4 Session 2, Sol10 P0-2), resolved exactly once via ResolveEnvironment before replay -- NewScope never loads or resolves the trusted-tool registry itself. Every call for one run (the run-level Scope and every stage's own Scope) must be handed the SAME env value, so the primitive actually launched can never diverge from the one the run's replay identity was computed against.
func ScopeFromContext ¶
ScopeFromContext retrieves a Scope attached by WithScope. Callers must treat "not found" as "no containment for this launch" -- every caller in this codebase falls back to the pre-S2 process-group behavior when this returns false, which only happens for launches that never went through a governed runtime.Runner (doctor probes, direct adapter tests).
func (*Scope) Command ¶
Command builds an *exec.Cmd for bin/args/dir such that the process -- and every descendant it forks, however it detaches -- is born inside this scope from the moment it starts. Callers still set Stdout/Stderr and call Start themselves; Command only owns argv/SysProcAttr/Dir.
rc4 Session 2 (Sol10 P0-2): the ScopeSystemdUserScope/ScopePIDNamespace branches used to exec s.primitivePath -- a canonical pathname re-resolved at every launch, the same TOCTOU shape Sol v9 P0-1/P0-2 already closed for enforce.Plan's unshare wrapper. A same-uid process could replace the file at that path between NewScope's verification and this exec, and the replacement -- not the verified binary -- would become the thing responsible for establishing containment.
An earlier version of this fix launched through s.primitiveHandle's own /proc/self/fd/<n> descriptor directly (mirroring toolregistry.Handle. Command). That broke every caller that composes a Scope's launch with enforce.Plan.Wrap's OWN independent fd-argv numbering (agents.LaunchStaged, internal/stage's default CommandFactory, and toolregistry.Handle. CommandWith's build callback all do, for backend/validator/bash launches under an active enforce.Plan) -- both layers independently assumed they would own ExtraFiles[0]/fd 3, so whichever layer's files got merged in second silently landed at the wrong descriptor (or, for CommandWith's build callback, got overwritten outright), and the argv string baked in by the other layer no longer pointed at what it meant to. Composing two independently fd-numbering launch mechanisms correctly would need every composition call site to thread a shared fd allocator through -- real production_launch_factory work belongs together, not scattered piecemeal under a fix for a single primitive.
So instead, both branches launch through a SEALED PRIVATE COPY: a fresh, 0500, same-uid-only-readable copy of s.primitiveHandle's own verified bytes (SealedExecutablePath, sealed FROM the already-open, already-hashed descriptor -- never by re-reading the enrolled path), re-verified (Verify) immediately before this launch to catch a same-uid tamper of the COPY itself between sealing and exec. This is one of the plan's own explicitly acceptable alternatives to fd-argv launch ("a verified private immutable copy"), it uses an ordinary real pathname so it composes with enforce.Plan.Wrap/Handle.CommandWith exactly like every other already-verified bin these callers pass through Scope.Command, and it needs no signature change here. The copy is owned by this Scope (s.sealedPrimitive) and closed by Extinguish, once the launched process has fully finished with it.
Sealing or verifying can fail (disk full, /tmp unwritable, or Verify genuinely catching a live same-uid tamper of the copy) -- Command has no error return, so a failure here produces a cmd that will simply fail at Start() with a descriptive, un-executable path, exactly the fail-closed outcome every caller's existing cmd.Start() error check already handles; it never falls back to a mutable pathname.
func (*Scope) CommandWith ¶
func (s *Scope) CommandWith(ctx context.Context, alloc *toolregistry.FDAllocator, bin string, args []string, dir string) *exec.Cmd
CommandWith is Command's composable, descriptor-backed form (Sol11 P0-5): a caller that must combine this Scope's own primitive launch (systemd-run/unshare) with another descriptor-backed layer of its own -- concretely, enforce.Plan.WrapWith's self-exec/unshare/final-executable descriptors -- passes one shared alloc so every layer's /proc/self/fd/<n> argv string lands at the fd number Start will actually dup it to, instead of two independently-numbered ExtraFiles lists colliding at fd 3. That collision is exactly why Command above still falls back to a sealed pathname copy of its primitive (see Command's own doc comment): it has no shared allocator to compose through. CommandWith closes the Verify-then-replace-then-exec race a sealed copy cannot by never reopening a pathname for the primitive at all -- it launches through s.primitiveHandle's own held, already-verified descriptor (/proc/self/fd/<n>, via alloc), the same object NewScope resolved and verified once, for the run's whole lifetime.
The caller must set the returned cmd's ExtraFiles to alloc.Files() once every composed layer -- including this one -- has finished registering.
func (*Scope) Extinguish ¶
func (s *Scope) Extinguish(ctx context.Context, deadline time.Duration, workspacePath string) (Proof, error)
Extinguish is the DESCENDANTS_TERMINATED lifecycle stage: freeze the scope so no new descendant is accepted, kill the whole owned tree, block until the kernel confirms zero surviving processes (bounded by deadline -- a timeout is a hard failure, never treated as "probably fine"), and finally scan every process's open file descriptors for a handle into workspacePath as an independent check that nothing escaped the primitive itself. The returned Proof is recorded on the run whether or not err is nil; a non-nil err means the run must not proceed to final-state capture.
func (*Scope) IsStrong ¶
IsStrong reports whether this Scope's underlying primitive actually owns its descendants (systemd-user-scope, cgroup-direct, pid-namespace) as opposed to the pre-S2 process-group-only degraded fallback. Exposed so a per-stage caller deriving a NEW, separate Scope for its own launch (Sol redteam v7 S1) can request the same strength the outer run-level Scope already achieved on this host, rather than needing its own independent copy of the run's requireStrong policy decision threaded through.
func (*Scope) Method ¶
func (s *Scope) Method() ScopeMethod
Method reports which primitive this Scope actually uses.
func (*Scope) RunID ¶
RunID returns the identifier this Scope was constructed with -- the same value the caller passed to NewScope. Exposed so a per-stage caller (Sol redteam v7 S1: a governed backend routed through internal/stage.Executor) can derive its own unique per-stage scope name from the SAME run identity the outer, run-level Scope already carries, without needing a second context key threaded alongside WithScope purely to repeat a value this Scope already has.
func (*Scope) Started ¶
Started must be called with the outer-namespace PID immediately after a successful Start() on the *exec.Cmd Command produced. It resolves the exact cgroup path (systemd assigns it asynchronously via its own IPC to the manager, so this polls briefly) or the PID-namespace's init PID, so Extinguish has something concrete to act on.
rc4 Session 2 (Sol10 P0-2): this used to close s.primitiveHandle here, on the reasoning that Start() had already dup'd the descriptor into the child so the parent's copy was no longer needed. That was correct when each Scope owned a freshly, independently resolved handle -- it is wrong now that primitiveHandle is BORROWED from the run's shared ContainmentEnvironment (every stage's own Scope launches through the same held descriptor across the run's whole lifetime); closing it here would close it out from under every later stage still to launch. Ownership of closing belongs solely to whoever resolved the ContainmentEnvironment (ResolveEnvironment's caller), once, after the run finishes.
type ScopeMethod ¶
type ScopeMethod string
ScopeMethod identifies which descendant-owning primitive backs a Scope.
const ( // ScopeSystemdUserScope wraps the launch in `systemd-run --user --scope`, // a transient cgroup v2 scope registered with the user's systemd manager. // Preferred: no filesystem write access to cgroupfs is required (systemd // already owns the delegation), and cleanup is automatic. ScopeSystemdUserScope ScopeMethod = "systemd-user-scope" // ScopeCgroupDirect creates a cgroup v2 subdirectory under the caller's // own cgroup and places the launched process into it atomically at // clone() time via CLONE_INTO_CGROUP (SysProcAttr.UseCgroupFD). Fallback // for hosts without a systemd user manager but where the caller's own // cgroup subtree is writable -- typically a container that owns its // whole cgroup namespace. ScopeCgroupDirect ScopeMethod = "cgroup-direct" // ScopePIDNamespace wraps the launch in `unshare --pid --fork // --mount-proc` with an identity uid/gid mapping so file ownership is // unaffected. Last resort: no cgroupfs access needed at all -- the // backend and everything it forks lives inside a PID namespace it cannot // leave, and the namespace's death is unconditional once its init // process (the wrapped command itself) exits or is killed. ScopePIDNamespace ScopeMethod = "pid-namespace" )