blob: 7df900c9650a9d938b19c2198df987ff72c7c049 [file] [log] [blame]
// 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", "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
}