internal/sandbox: validate config

Add Sandbox.Validate, to check the bundle's config.json.
Most importantly, check that bind mount source directories exist.

Make Output call Validate.

Also call it from the worker, with logging to debug that it's
happening.

Change-Id: I53b93470dbc65eb8fb5c7d313b939e841f457d89
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/477077
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go
index a239cc2..7df900c 100644
--- a/internal/sandbox/sandbox.go
+++ b/internal/sandbox/sandbox.go
@@ -9,7 +9,9 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"os"
 	"os/exec"
+	"path/filepath"
 
 	"golang.org/x/pkgsite-metrics/internal/derrors"
 )
@@ -83,6 +85,9 @@
 // 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")
@@ -110,3 +115,54 @@
 	}
 	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
+}
diff --git a/internal/sandbox/sandbox_test.go b/internal/sandbox/sandbox_test.go
index 8bb163c..b24dfa4 100644
--- a/internal/sandbox/sandbox_test.go
+++ b/internal/sandbox/sandbox_test.go
@@ -24,6 +24,9 @@
 	}
 	sb := New("testdata/bundle")
 	sb.Runsc = "/usr/local/bin/runsc" // must match path in Makefile
+	if err := sb.Validate(); err != nil {
+		t.Fatal(err)
+	}
 
 	check := func(t *testing.T, cmd *Cmd, want string) {
 		t.Helper()
@@ -78,3 +81,13 @@
 		}
 	})
 }
+
+func TestValidate(t *testing.T) {
+	// Validate doesn't actually run the sandbox, so we can test it.
+	t.Skip("fails in gcloud build")
+	sb := New("testdata/bundle")
+	sb.Runsc = "/usr/local/bin/runsc"
+	if err := sb.Validate(); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/internal/sandbox/testdata/bundle/config.json b/internal/sandbox/testdata/bundle/config.json
index 1bd2beb..67c7a2e 100644
--- a/internal/sandbox/testdata/bundle/config.json
+++ b/internal/sandbox/testdata/bundle/config.json
@@ -75,6 +75,12 @@
 				"nodev",
 				"ro"
 			]
+		},
+		{
+			"destination": "/tmp/foo",
+			"type": "none",
+			"source": "/",
+			"options": ["bind"]
 		}
 	],
 	"linux": {
diff --git a/internal/worker/govulncheck_scan.go b/internal/worker/govulncheck_scan.go
index fe93283..ed4ba1d 100644
--- a/internal/worker/govulncheck_scan.go
+++ b/internal/worker/govulncheck_scan.go
@@ -328,6 +328,12 @@
 
 	log.Infof(ctx, "running govulncheck in sandbox: %s@%s", modulePath, version)
 	smdir := strings.TrimPrefix(mdir, sandboxRoot)
+	err = s.sbox.Validate()
+	log.Debugf(ctx, "sandbox Validate returned %v", err)
+	if err != nil {
+		return nil, err
+	}
+
 	stdout, err := s.sbox.Command(binaryDir+"/govulncheck_sandbox", govulncheckPath, ModeGovulncheck, smdir).Output()
 	log.Infof(ctx, "done with govulncheck in sandbox: %s@%s err=%v", modulePath, version, err)