buildstep.Isolation and Isolated: its own namespaces with only a loopback, an environment of exactly what the step is given, the system and toolchain read-only, the workspace writable, and no home, key, bus login, runtime directory or name service.
116 lines
4.2 KiB
Go
116 lines
4.2 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// pairs says whether argv holds the words of want one after another.
|
|
func holds(argv []string, want ...string) bool {
|
|
for i := 0; i+len(want) <= len(argv); i++ {
|
|
match := true
|
|
for j, w := range want {
|
|
if argv[i+j] != w {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
if match {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func fixedSystem(t *testing.T) {
|
|
t.Helper()
|
|
before := systemArgs
|
|
systemArgs = func() []string { return []string{"--ro-bind", "/usr", "/usr", "--symlink", "usr/bin", "/bin"} }
|
|
t.Cleanup(func() { systemArgs = before })
|
|
}
|
|
|
|
// A run step's command runs in bubblewrap (novox/hq ADR 0448; issue 1309): no network, no namespace shared, an
|
|
// environment of exactly what the step is given, the workspace the one tree it writes, the toolchain read-only, and
|
|
// nothing else of the machine's: not the account's home, not the forge key or a bus login, not the runtime directory,
|
|
// and no name service.
|
|
func TestARunStepRunsShutOffInBubblewrap(t *testing.T) {
|
|
fixedSystem(t)
|
|
iso := Isolation{Workspace: "/work/ab12/tree", ReadOnly: []string{"/usr/lib/go"}, Writable: []string{"/work/ab12/out"},
|
|
Env: []string{"GOFLAGS=-mod=mod", "PATH=/usr/bin"}, Hosts: "/work/ab12/hosts"}
|
|
argv, err := iso.Command("go", "test", "./...")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range [][]string{
|
|
{"--unshare-all"}, {"--die-with-parent"}, {"--new-session"}, {"--clearenv"},
|
|
{"--proc", "/proc"}, {"--dev", "/dev"}, {"--tmpfs", "/tmp"},
|
|
{"--ro-bind", "/usr", "/usr"}, {"--symlink", "usr/bin", "/bin"},
|
|
{"--ro-bind", "/usr/lib/go", "/usr/lib/go"},
|
|
{"--bind", "/work/ab12/tree", "/work/ab12/tree"}, {"--bind", "/work/ab12/out", "/work/ab12/out"},
|
|
{"--ro-bind", "/work/ab12/hosts", "/etc/hosts"},
|
|
{"--setenv", "GOFLAGS", "-mod=mod"}, {"--setenv", "PATH", "/usr/bin"}, {"--setenv", "HOME", "/tmp/home"},
|
|
{"--chdir", "/work/ab12/tree"},
|
|
{"--", "go", "test", "./..."},
|
|
} {
|
|
if !holds(argv, want...) {
|
|
t.Errorf("the command has no %v: %v", want, argv)
|
|
}
|
|
}
|
|
if holds(argv, "--share-net") || strings.Contains(strings.Join(argv, " "), "resolv.conf") || strings.Contains(strings.Join(argv, " "), "/run") {
|
|
t.Errorf("the command reaches the network, the name service or the runtime directory: %v", argv)
|
|
}
|
|
// The workspace is mounted after /tmp's tmpfs, so a workspace under /tmp is not hidden by it.
|
|
if strings.Index(strings.Join(argv, "\x00"), "--tmpfs\x00/tmp") > strings.Index(strings.Join(argv, "\x00"), "--bind\x00/work/ab12/tree") {
|
|
t.Errorf("the tmpfs of /tmp comes after the workspace: %v", argv)
|
|
}
|
|
// A HOME the step is given is its own.
|
|
iso.Env = append(iso.Env, "HOME=/work/ab12/home")
|
|
argv, _ = iso.Command("true")
|
|
if holds(argv, "--setenv", "HOME", "/tmp/home") || !holds(argv, "--setenv", "HOME", "/work/ab12/home") {
|
|
t.Errorf("the step's own HOME was replaced: %v", argv)
|
|
}
|
|
}
|
|
|
|
func TestAnIsolationNamesNoPathItCannotPin(t *testing.T) {
|
|
for _, iso := range []Isolation{
|
|
{Workspace: ""},
|
|
{Workspace: "tree"},
|
|
{Workspace: "/"},
|
|
{Workspace: "/w", ReadOnly: []string{"relative"}},
|
|
{Workspace: "/w", Writable: []string{"/"}},
|
|
{Workspace: "/w", Caches: []string{"/a/../b"}},
|
|
{Workspace: "/w", Env: []string{"NOEQUALS"}},
|
|
} {
|
|
if _, err := iso.Command("true"); err == nil {
|
|
t.Errorf("%+v was accepted", iso)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Isolated runs every command through bwrap, with the step's environment inside and a hosts file that names only the
|
|
// loopback, removed after.
|
|
func TestIsolatedRunsEachCommandThroughBubblewrap(t *testing.T) {
|
|
fixedSystem(t)
|
|
var ran []string
|
|
var hosts string
|
|
inner := func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
ran = append([]string{name}, args...)
|
|
for i, a := range args {
|
|
if a == "/etc/hosts" && i > 0 {
|
|
hosts = args[i-1]
|
|
}
|
|
}
|
|
return forgekey.Ran{Stdout: "ok"}
|
|
}
|
|
r := Isolated(inner, Isolation{Workspace: "/w"})(context.Background(), []string{"A=1"}, "go", "build")
|
|
if r.Stdout != "ok" || ran[0] != Bubblewrap || !holds(ran, "--setenv", "A", "1") || !holds(ran, "--", "go", "build") {
|
|
t.Fatalf("ran %v: %+v", ran, r)
|
|
}
|
|
if hosts == "" {
|
|
t.Fatalf("no hosts file was bound: %v", ran)
|
|
}
|
|
}
|