diff --git a/docs/security.md b/docs/security.md index 4fd8660..331d503 100644 --- a/docs/security.md +++ b/docs/security.md @@ -24,6 +24,10 @@ sandbox that ends with the run: read-only, a fresh home directory that is deleted after the run, a per-repository cache directory, the harness's login file, and the program directories the run needs. System directories are read-only. + The Go toolchains in your module download cache + (`golang.org/toolchain`) are readable too, so a repository that needs + a newer Go builds without a download; other cached modules stay + hidden. The run can read everything in the repository's `.git`: local branches, stashes and `.git/config`. Do not keep a token in a remote URL. diff --git a/sandbox.go b/sandbox.go index e466d9a..b56860d 100644 --- a/sandbox.go +++ b/sandbox.go @@ -59,6 +59,7 @@ type sandboxSpec struct { programDirs []string extraBinds []string localPorts []int + goToolchains string ghafkBin string path string profile string @@ -499,6 +500,7 @@ func buildSandbox(o sandboxOpts) (sandboxSpec, error) { spec.passEnv = cfg.env spec.machineEgress = cfg.egress spec.localPorts = cfg.local + spec.goToolchains = goToolchainCache(home) missing := []string{} for _, w := range words { if w == "" { @@ -1095,6 +1097,30 @@ func sandboxEnv(spec sandboxSpec, home, proxyURL string) []string { return env } +func goToolchainCache(home string) string { + modCache := os.Getenv("GOMODCACHE") + if modCache == "" { + gopath := filepath.Join(home, "go") + if list := filepath.SplitList(os.Getenv("GOPATH")); len(list) > 0 && list[0] != "" { + gopath = list[0] + } + modCache = filepath.Join(gopath, "pkg", "mod") + } + dir := filepath.Join(modCache, "cache", "download", "golang.org", "toolchain") + if info, err := os.Stat(dir); err != nil || !info.IsDir() { + return "" + } + return dir +} + +func sandboxGoProxy(toolchains string) string { + if toolchains == "" { + return "https://proxy.golang.org,direct" + } + root := filepath.Dir(filepath.Dir(toolchains)) + return "file://" + filepath.ToSlash(root) + ",https://proxy.golang.org,direct" +} + func sandboxFixedEnv(spec sandboxSpec, home, proxyURL string) []string { tmp := filepath.Join(home, "tmp") env := []string{ @@ -1116,6 +1142,7 @@ func sandboxFixedEnv(spec sandboxSpec, home, proxyURL string) []string { "npm_config_store_dir=" + filepath.Join(spec.cacheDir, "pnpm"), "pnpm_config_store_dir=" + filepath.Join(spec.cacheDir, "pnpm"), "pnpm_config_cache_dir=" + filepath.Join(spec.cacheDir, "pnpm-cache"), + "GOPROXY=" + sandboxGoProxy(spec.goToolchains), "GOPATH=" + filepath.Join(home, "go"), "CARGO_HOME=" + filepath.Join(home, ".cargo"), "PIP_CACHE_DIR=" + filepath.Join(home, ".cache", "pip"), @@ -1367,6 +1394,12 @@ func seatbeltPaths(spec sandboxSpec) seatbeltSpec { s.Execs = append(s.Execs, devExecs...) s.Reads = append(s.Reads, devReads...) s.LocalPorts = spec.localPorts + if spec.goToolchains != "" { + s.DataReads = append(s.DataReads, evalPath(spec.goToolchains)) + if r := evalPath(spec.goToolchains); r != spec.goToolchains { + s.Links = append(s.Links, spec.goToolchains) + } + } if spec.repoGit != "" { s.DataReads = append(s.DataReads, evalPath(spec.repoGit)) if r := evalPath(spec.repoGit); r != spec.repoGit { diff --git a/sandbox_linux.go b/sandbox_linux.go index 79d26a9..9c28a99 100644 --- a/sandbox_linux.go +++ b/sandbox_linux.go @@ -135,6 +135,9 @@ func bwrapArgs(spec sandboxSpec, port int, dir, command string) []string { for _, dir := range spec.extraBinds { args = append(args, "--ro-bind", dir, dir) } + if spec.goToolchains != "" { + args = append(args, "--ro-bind", spec.goToolchains, spec.goToolchains) + } if spec.repoGit != "" { args = append(args, "--ro-bind", spec.repoGit, spec.repoGit) } diff --git a/sandbox_run_test.go b/sandbox_run_test.go index 5d55654..9a6b02d 100644 --- a/sandbox_run_test.go +++ b/sandbox_run_test.go @@ -466,3 +466,36 @@ func TestALocalPortIsReachableOnlyWhenTheConfigNamesIt(t *testing.T) { t.Fatalf("a local: port must reach the service on this machine: %q", out) } } + +func TestTheSandboxReadsOnlyTheCachedGoToolchains(t *testing.T) { + needSandbox(t) + t.Setenv("GOMODCACHE", "") + t.Setenv("GOPATH", "") + home := t.TempDir() + download := filepath.Join(home, "go", "pkg", "mod", "cache", "download") + toolchains := filepath.Join(download, "golang.org", "toolchain") + private := filepath.Join(download, "example.com", "private") + for dir, file := range map[string]string{toolchains: "toolchain-ok", private: "private-source"} { + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "marker"), []byte(file), 0o644); err != nil { + t.Fatal(err) + } + } + var out bytes.Buffer + err := runSandboxed(sandboxOpts{ + name: "checks", dir: t.TempDir(), timeout: 30 * time.Second, home: home, + command: fmt.Sprintf(`cat %q; echo; cat %q 2>/dev/null || echo private-hidden; echo "proxy=$GOPROXY"`, filepath.Join(toolchains, "marker"), filepath.Join(private, "marker")), + }, &out, &out) + if err != nil { + t.Fatal(err) + } + got := out.String() + if !strings.Contains(got, "toolchain-ok") || !strings.Contains(got, "proxy=file://") { + t.Fatalf("the cached Go toolchains must be readable through a file proxy, or a repo that needs a newer Go cannot build: %q", got) + } + if strings.Contains(got, "private-source") || !strings.Contains(got, "private-hidden") { + t.Fatalf("only the toolchains may be visible, not other cached modules: %q", got) + } +}