native

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package native implements container.Backend by running commands directly on the host via os/exec. It ignores the IR's image: field and refuses compose: containers. Native runs are resumable — snapshot: workspace workdirs are restored from a full gzip-tar archive on resume; the host base environment is not pinned, so native remains explicitly out-of-spec for digest-pinned reproducibility.

Design: docs/superpowers/specs/2026-04-20-awf-phase4-slice-4-7-design.md

/tmp/awf bootstrap: New() creates host /tmp/awf because the engine dispatcher writes AWF_OUTPUT to /tmp/awf/<step>.json (engine/awf_output.go:24-26) and the author's shell step `... > $AWF_OUTPUT` requires the directory to exist. Docker doesn't need this (each container's /tmp is fresh).

CaptureFiles `..` traversal: resolves via filepath.Join's cleaning rules. A path like ../etc/passwd reads from the host literally. Consistent with the no-isolation design (Cmd.Run is equally unconstrained).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyLandlock

func ApplyLandlock(roDirs, rwDirs []string) error

ApplyLandlock enforces a Landlock FS policy in the current process. Called by the __sandbox trampoline (native.MaybeRunSandboxTrampoline) BEFORE syscall.Exec.

Policy:

  • RODirs: read-only access (no write, rename, or truncate).
  • RWDirs: full read+write access.
  • IgnoreIfMissing: silently skips paths that do not exist on the host (newly-provisioned machines may lack optional dirs like /lib64).

Fail-closed contract: we refuse to run a "sandboxed" step on a kernel with NO Landlock support (ABI 0) — silently executing on the unrestricted host would violate the faithful-delivery guarantee (promised isolation we can't provide = loud failure, never silent host execution). But the confinement we need is only FS path restriction (RODirs/RWDirs), which EVERY Landlock ABI (v1+) enforces. So we apply with BestEffort, which downgrades from the newest ABI to whatever the running kernel actually supports and still restricts the filesystem. (Hard-pinning a specific newest ABI like V9 would wrongly reject every kernel between v1 and v8, even though they can fully enforce our policy — the CI runner is exactly such a kernel.) The explicit ABI>=1 guard ensures BestEffort never degrades to a silent no-op on a Landlock-less kernel.

Note: RestrictPaths also sets the "no new privileges" flag in the calling process, which is appropriate for untrusted step execution.

func MaybeRunSandboxTrampoline

func MaybeRunSandboxTrampoline()

MaybeRunSandboxTrampoline inspects os.Args; if this process was re-exec'd as "<self> __sandbox <policyJSON> -- sh -c <run>" it applies the Landlock policy and replaces the process with /bin/sh running the step command (it never returns). Otherwise it returns immediately and the caller continues normally (the common case).

Types

type Backend

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

Backend implements container.Backend by spawning host processes via os/exec. Single concrete impl; not safe for concurrent runs of the same workflow (see slice 4.7 design spec Appendix E for the /tmp/awf/<step>.json clobber failure mode).

func New

func New(workdirRoot string, opts ...Option) (*Backend, error)

New constructs a Backend with workdirRoot as the parent for per- container workdirs. Also bootstraps host /tmp/awf (idempotent). Validates only nil/empty workdirRoot — filesystem errors surface lazily from Create's MkdirAll, matching docker's pattern.

Edge case: pre-existing /tmp/awf with restrictive perms (e.g., root-owned 0o700) is NOT detected here — first Exec fails with shell "permission denied" instead.

func (*Backend) Capabilities

func (b *Backend) Capabilities() container.Caps

Capabilities reports SnapshotFSArchive iff blobs were supplied (WithBlobs), else SnapshotNone — native implements Snapshot/Restore only against an injected CAS store. Without it, workflows with snapshot:workspace containers must use --backend docker.

func (*Backend) CaptureFiles

func (b *Backend) CaptureFiles(ctx context.Context, h container.Handle, paths []string) ([]container.CapturedFile, error)

CaptureFiles reads the named in-container paths and returns their content. Relative paths resolve via filepath.Join against the handle's workdir; absolute paths are used literally on the host filesystem (no chroot — consistent with the no-isolation design, decision 10). Missing-path is a hard error (no partial returns) matching docker's CaptureFiles behavior.

CaptureFiles `..` traversal: filepath.Join cleans `..` segments, so `CaptureFiles(h, []string{"../etc/passwd"})` resolves to `<host>/etc/passwd`. This is expected behavior — Cmd.Run is equally unconstrained.

func (*Backend) CopyTo

func (b *Backend) CopyTo(ctx context.Context, h container.Handle, files []container.InputFile) error

CopyTo writes each InputFile to the host filesystem — the symmetric inverse of CaptureFiles. Path resolution matches CaptureFiles: absolute paths are used literally on the host (no chroot — the no-isolation design, decision 10), relative paths resolve via filepath.Join against the handle's workdir. Parent directories are created (0o755) so a nested destination "just works", mirroring docker's auto-mkdir. Overwrites an existing file. len(files)==0 is a no-op; unknown handle is a hard error. Never touches state.Blobs.

`..` traversal: filepath.Join cleans `..` segments, so a destination like "../etc/foo" writes to the host literally. Consistent with CaptureFiles and Cmd.Run, which are equally unconstrained.

func (*Backend) Create

Create rejects compose-mode (no service-routing on host), ignores spec.Image (native is not digest-pinned per decision 1), and creates <workdirRoot>/<spec.Name>/ as the per-container workdir. The Handle's ID carries the workdir path so Exec/CaptureFiles/Destroy can resolve it via the handles map.

func (*Backend) Destroy

func (b *Backend) Destroy(ctx context.Context, h container.Handle) error

Destroy removes the handle's workdir and unregisters the handle. Returns an error on double-destroy (matches docker / os.File.Close convention). os.RemoveAll is idempotent on ENOENT, so a workdir that's already gone (e.g., a step deleted it) succeeds.

func (*Backend) Exec

func (b *Backend) Exec(ctx context.Context, h container.Handle, cmd container.Cmd) (<-chan container.IOChunk, <-chan container.ExecResult, error)

Exec runs cmd.Run via `sh -c` with cmd.Dir = handle workdir. Host env is inherited and merged with cmd.Env (cmd.Env wins on conflict). Stdout is accumulated into ExecResult.Stdout; stderr surfaces only via the IOChunk channel (per AWF spec §4.1 — implicit outputs are exit_code and stdout only).

Streaming contract (slice 5.3): returns (chunks, result, error). Per-pipe reader goroutines emit IOChunks live as bytes arrive; a waiter goroutine wg.Waits both readers, then c.Wait()s the process, closes chunks, computes the ExitCode and emits ONE ExecResult on the 1-buffered result channel. chunks closes BEFORE result emits — every chunk is materialized before the result is observable.

ctx-cancel: Go 1.20+ exec.CommandContext sets Cmd.Cancel = cmd.Process.Kill by default. ctx.Done() -> SIGKILL -> pipes close -> reader goroutines exit -> wg.Wait returns. Pre-slice-5.3 the ctx.Err came back via Backend.Exec's err return; the streaming refactor surfaces it via ExecResult.Err instead so the result channel can carry it without short-circuiting the chunk drain. c.Wait's error is discarded; ProcessState.ExitCode() is the meaningful signal (matches docker's ContainerExecInspect pattern and pre-5.3 native behavior).

Phase 2 + slice 5.3 contract: on non-nil err return, BOTH channels are nil (callers MUST check err before receiving on either).

func (*Backend) Restore

func (b *Backend) Restore(ctx context.Context, ref container.SnapshotRef, name string) (container.Handle, error)

Restore re-materializes a container workdir from a SnapshotRef. EVERY filesystem op goes through one os.Root rooted at the (trusted, fixed) workdir, so a '..' name or an attacker-planted/captured symlink cannot redirect a write outside the workdir. The confinement is enforced by os.Root at whichever op the malicious path hits first — the parent MkdirAll (e.g. "evil/x" after an "evil"->outside symlink trips EEXIST at ensureParent) or the create/symlink itself (TOCTOU-safe at openat; the same primitive loader.go uses). Perms are set AT CREATE (no post-create Root.Chmod) to avoid CVE-2026-32282.

func (*Backend) SandboxWarnLabel

func (b *Backend) SandboxWarnLabel() string

SandboxWarnLabel returns a non-empty warning string when the backend was constructed with WithSandbox(true) but no platform sandbox was available (i.e. the no-op fallback is in use). Returns "" when sandbox is disabled or when a real platform launcher was selected.

The CLI MUST print this to stderr immediately after constructing the native backend, in the same style as the no-image warning at cli/run.go:354:

awf run: --backend native: <SandboxWarnLabel()>

func (*Backend) Snapshot

Snapshot captures the container workdir as a deterministic gzip-tar blob. Enforces TWO caps: the compressed-output cap (cappedWriter on the gzip stream) and the decompressed-total cap (uncompressed bytes fed in) — the latter symmetric with Restore so an unrestorable snapshot cannot be created.

type Option

type Option func(*Backend) error

Option configures a native Backend at construction.

func WithBlobs

func WithBlobs(b state.Blobs) Option

WithBlobs supplies the CAS store Snapshot/Restore use. Without it the backend advertises SnapshotNone and Snapshot returns a descriptive error.

func WithSandbox

func WithSandbox(enabled bool) Option

WithSandbox enables (enabled=true) or disables (enabled=false) OS-level process sandboxing for native backend Exec calls. When enabled, detectSandbox resolves the best available platform launcher (bwrap on Linux, sandbox-exec on macOS). If no platform sandbox is available the backend falls back to a no-op launcher and SandboxWarnLabel returns a non-empty loud warning string that the CLI MUST surface to stderr (mirror of cli/run.go:354 pattern).

When disabled (or not supplied), sandbox detection is skipped entirely and SandboxWarnLabel returns "".

func WithSnapshotMaxBlobBytes

func WithSnapshotMaxBlobBytes(n int64) Option

WithSnapshotMaxBlobBytes overrides the compressed snapshot-blob cap (default snapshotDefaultMaxBlobBytes). n must be > 0.

func WithSnapshotMaxRestoreBytes

func WithSnapshotMaxRestoreBytes(n int64) Option

WithSnapshotMaxRestoreBytes overrides the cumulative decompressed-bytes cap (default snapshotDefaultMaxRestoreBytes). n must be > 0.

type SandboxPolicy

type SandboxPolicy struct {
	RODirs []string `json:"ro_dirs"`
	RWDirs []string `json:"rw_dirs"`
	Run    string   `json:"run"`
}

SandboxPolicy is the JSON payload the __sandbox trampoline subcommand decodes. It is exported so cmd/awf/sandbox_linux.go can decode it without duplicating the struct definition.

Jump to

Keyboard shortcuts

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