| // Copyright 2022 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package sandbox runs programs in a secure environment. |
| package sandbox |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "os" |
| "os/exec" |
| "path/filepath" |
| |
| "golang.org/x/pkgsite-metrics/internal/derrors" |
| ) |
| |
| // A Sandbox is a restricted execution environment. |
| // A Sandbox instance refers to a directory containing an OCI |
| // bundle (see https://github.com/opencontainers/runtime-spec/blob/main/bundle.md). |
| type Sandbox struct { |
| bundleDir string |
| Runsc string // path to runsc program |
| } |
| |
| // New returns a new Sandbox using the bundle in bundleDir. |
| // The bundle must be configured to run the 'runner' program, |
| // built from runner.go in this directory. |
| // The Sandbox expects the runsc program to be on the path. |
| // That can be overridden by setting the Runsc field. |
| func New(bundleDir string) *Sandbox { |
| return &Sandbox{ |
| bundleDir: bundleDir, |
| Runsc: "runsc", |
| } |
| } |
| |
| // Cmd's exported fields must be a subset of the exported fields of exec.Cmd. |
| // runner.go must be able to unmarshal a sandbox.Cmd into an exec.Cmd. |
| |
| // Cmd describes how to run a binary in a sandbox. |
| type Cmd struct { |
| sb *Sandbox |
| |
| // Path is the path of the command to run. |
| // |
| // This is the only field that must be set to a non-zero |
| // value. If Path is relative, it is evaluated relative |
| // to Dir. |
| Path string |
| |
| // Args holds command line arguments, including the command as Args[0]. |
| // If the Args field is empty or nil, Run uses {Path}. |
| // |
| // In typical use, both Path and Args are set by calling Command. |
| Args []string |
| |
| // Env specifies the environment of the process. |
| // Each entry is of the form "key=value". |
| // If Env is nil, the new process uses whatever environment |
| // runsc provides by default. |
| Env []string |
| |
| // If AppendToEnv is true, the contents of Env are appended |
| // to the sandbox's existing environment, instead of replacing it. |
| AppendToEnv bool |
| |
| // Dir specifies the working directory of the command. |
| // If Dir is the empty string, Run runs the command in the |
| // root of the sandbox filesystem. |
| Dir string |
| } |
| |
| // Command creates a *Cmd to run path in the sandbox. |
| // It behaves like [os/exec.Command]. |
| func (s *Sandbox) Command(path string, arg ...string) *Cmd { |
| return &Cmd{ |
| sb: s, |
| Path: path, |
| Args: append([]string{path}, arg...), |
| } |
| } |
| |
| // Output runs Cmd in the sandbox used to create it, and returns its standard output. |
| func (c *Cmd) Output() (_ []byte, err error) { |
| defer derrors.Wrap(&err, "Cmd.Output %q", c.Args) |
| if err := c.sb.Validate(); err != nil { |
| return nil, err |
| } |
| // -ignore-cgroups is needed to avoid this error from runsc: |
| // cannot set up cgroup for root: configuring cgroup: write /sys/fs/cgroup/cgroup.subtree_control: device or resource busy |
| cmd := exec.Command(c.sb.Runsc, "-ignore-cgroups", "-network=none", "-platform=systrap", "-dcache=500", "run", "sandbox") |
| cmd.Dir = c.sb.bundleDir |
| stdinPipe, err := cmd.StdinPipe() |
| if err != nil { |
| return nil, err |
| } |
| stdin, err := json.Marshal(c) |
| if err != nil { |
| return nil, err |
| } |
| ch := make(chan error, 1) |
| go func() { |
| _, err := stdinPipe.Write(stdin) |
| stdinPipe.Close() |
| ch <- err |
| }() |
| out, err := cmd.Output() |
| if err != nil { |
| return nil, err |
| } |
| if err := <-ch; err != nil { |
| return nil, fmt.Errorf("writing stdin: %w", err) |
| } |
| return bytes.TrimSpace(out), nil |
| } |
| |
| // ociConfig is a subset of the OCI container configuration. |
| // It is used by Validate to unmarshal the bundle's config.json. |
| type ociConfig struct { |
| Version string `json:"ociVersion"` |
| Mounts []mount `json:"mounts"` |
| } |
| |
| type mount struct { |
| Destination string `json:"destination"` |
| Type string `json:"type"` |
| Source string `json:"source"` |
| Options []string `json:"options"` |
| } |
| |
| // Validate the sandbox configuration. |
| func (s *Sandbox) Validate() (err error) { |
| defer derrors.Wrap(&err, "Sandbox(%s).Validate()", s.bundleDir) |
| |
| f, err := os.Open(filepath.Join(s.bundleDir, "config.json")) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| var config ociConfig |
| if err := json.NewDecoder(f).Decode(&config); err != nil { |
| return err |
| } |
| const wantVersion = "1.0.0" |
| if config.Version != wantVersion { |
| return fmt.Errorf("ociVersion: got %q, want %q", config.Version, wantVersion) |
| } |
| for _, m := range config.Mounts { |
| if isBindMount(m) { |
| _, err := os.Stat(m.Source) |
| if err != nil { |
| return fmt.Errorf("bind mount source: %w", err) |
| } |
| } |
| } |
| return nil |
| } |
| |
| func isBindMount(m mount) bool { |
| for _, opt := range m.Options { |
| if opt == "bind" { |
| return true |
| } |
| } |
| return false |
| } |