Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type sandboxSpec struct {
programDirs []string
extraBinds []string
localPorts []int
goToolchains string
ghafkBin string
path string
profile string
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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{
Expand All @@ -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"),
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions sandbox_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
33 changes: 33 additions & 0 deletions sandbox_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading