//go:build linux // +build linux package chroot import ( "bytes" "encoding/json" "fmt" "io" "os" "path/filepath" "strconv" "strings" "testing" "github.com/containers/buildah/tests/testreport/types" "github.com/containers/buildah/util" "github.com/containers/storage/pkg/mount" "github.com/containers/storage/pkg/reexec" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) const ( reportCommand = "testreport" ) func TestMain(m *testing.M) { if reexec.Init() { return } os.Exit(m.Run()) } func testMinimal(t *testing.T, modify func(g *generate.Generator, rootDir, bundleDir string), verify func(t *testing.T, report *types.TestReport)) { t.Helper() g, err := generate.New("linux") if err != nil { t.Fatalf("generate.New(%q): %v", "linux", err) } if err = setupSeccomp(g.Config, ""); err != nil { t.Fatalf("setupSeccomp(%q): %v", "", err) } // t.TempDir returns /tmp/TestName/001. // /tmp/TestName/001 has permission 0777, but /tmp/TestName is 0700 tempDir := t.TempDir() if err = os.Chmod(filepath.Dir(tempDir), 0711); err != nil { t.Fatalf("error loosening permissions on %q: %v", tempDir, err) } rootDir := filepath.Join(tempDir, "root") if err := os.Mkdir(rootDir, 0711); err != nil { t.Fatalf("os.Mkdir(%q): %v", rootDir, err) } rootTmpDir := filepath.Join(rootDir, "tmp") if err := os.Mkdir(rootTmpDir, 01777); err != nil { t.Fatalf("os.Mkdir(%q): %v", rootTmpDir, err) } specPath := filepath.Join("..", "tests", reportCommand, reportCommand) specBinarySource, err := os.Open(specPath) if err != nil { t.Fatalf("open(%q): %v", specPath, err) } defer specBinarySource.Close() specBinary, err := os.OpenFile(filepath.Join(rootDir, reportCommand), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0711) if err != nil { t.Fatalf("open(%q): %v", filepath.Join(rootDir, reportCommand), err) } if _, err := io.Copy(specBinary, specBinarySource); err != nil { t.Fatalf("io.Copy error: %v", err) } specBinary.Close() g.SetRootPath(rootDir) g.SetProcessArgs([]string{"/" + reportCommand}) bundleDir := filepath.Join(tempDir, "bundle") if err := os.Mkdir(bundleDir, 0700); err != nil { t.Fatalf("os.Mkdir(%q): %v", bundleDir, err) } if modify != nil { modify(&g, rootDir, bundleDir) } uid, gid, err := util.GetHostRootIDs(g.Config) if err != nil { t.Fatalf("GetHostRootIDs: %v", err) } if err := os.Chown(rootDir, int(uid), int(gid)); err != nil { t.Fatalf("os.Chown(%q): %v", rootDir, err) } output := new(bytes.Buffer) if err := RunUsingChroot(g.Config, bundleDir, "/", new(bytes.Buffer), output, output); err != nil { t.Fatalf("run: %v: %s", err, output.String()) } var report types.TestReport if err := json.Unmarshal(output.Bytes(), &report); err != nil { t.Fatalf("decode: %v", err) } if verify != nil { verify(t, &report) } } func TestNoop(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, nil, nil) } func TestMinimalSkeleton(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { }, func(t *testing.T, report *types.TestReport) { }, ) } func TestProcessTerminal(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, terminal := range []bool{false, true} { testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.SetProcessTerminal(terminal) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.Terminal != terminal { t.Fatalf("expected terminal = %v, got %v", terminal, report.Spec.Process.Terminal) } }, ) } } func TestProcessConsoleSize(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, size := range [][2]uint{{80, 25}, {132, 50}} { testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.SetProcessTerminal(true) g.SetProcessConsoleSize(size[0], size[1]) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.ConsoleSize.Width != size[0] { t.Fatalf("expected console width = %v, got %v", size[0], report.Spec.Process.ConsoleSize.Width) } if report.Spec.Process.ConsoleSize.Height != size[1] { t.Fatalf("expected console height = %v, got %v", size[1], report.Spec.Process.ConsoleSize.Height) } }, ) } } func TestProcessUser(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, id := range []uint32{0, 1000} { testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.SetProcessUID(id) g.SetProcessGID(id + 1) g.AddProcessAdditionalGid(id + 2) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.User.UID != id { t.Fatalf("expected UID %v, got %v", id, report.Spec.Process.User.UID) } if report.Spec.Process.User.GID != id+1 { t.Fatalf("expected GID %v, got %v", id+1, report.Spec.Process.User.GID) } }, ) } } func TestProcessEnv(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } e := fmt.Sprintf("PARENT_TEST_PID=%d", unix.Getpid()) testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.ClearProcessEnv() g.AddProcessEnv("PARENT_TEST_PID", strconv.Itoa(unix.Getpid())) }, func(t *testing.T, report *types.TestReport) { for _, ev := range report.Spec.Process.Env { if ev == e { return } } t.Fatalf("expected environment variable %q", e) }, ) } func TestProcessCwd(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { if err := os.Mkdir(filepath.Join(rootDir, "/no-such-directory"), 0700); err != nil { t.Fatalf("mkdir(%q): %v", filepath.Join(rootDir, "/no-such-directory"), err) } g.SetProcessCwd("/no-such-directory") }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.Cwd != "/no-such-directory" { t.Fatalf("expected %q, got %q", "/no-such-directory", report.Spec.Process.Cwd) } }, ) } func TestProcessCapabilities(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.ClearProcessCapabilities() }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Process.Capabilities.Permitted) != 0 { t.Fatalf("expected no permitted capabilities, got %#v", report.Spec.Process.Capabilities.Permitted) } }, ) testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.ClearProcessCapabilities() if err := g.AddProcessCapabilityEffective("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityPermitted("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityInheritable("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityBounding("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } if err := g.AddProcessCapabilityAmbient("CAP_IPC_LOCK"); err != nil { t.Fatalf("%v", err) } }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Process.Capabilities.Permitted) != 1 { t.Fatalf("expected one permitted capability, got %#v", report.Spec.Process.Capabilities.Permitted) } if report.Spec.Process.Capabilities.Permitted[0] != "CAP_IPC_LOCK" { t.Fatalf("expected one capability CAP_IPC_LOCK, got %#v", report.Spec.Process.Capabilities.Permitted) } }, ) } func TestProcessRlimits(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, limit := range []uint64{100 * 1024 * 1024 * 1024, 200 * 1024 * 1024 * 1024, unix.RLIM_INFINITY} { testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.ClearProcessRlimits() if limit != unix.RLIM_INFINITY { g.AddProcessRlimits("rlimit_as", limit, limit) } }, func(t *testing.T, report *types.TestReport) { var rlim *specs.POSIXRlimit for i := range report.Spec.Process.Rlimits { if strings.ToUpper(report.Spec.Process.Rlimits[i].Type) == "RLIMIT_AS" { rlim = &report.Spec.Process.Rlimits[i] } } if limit == unix.RLIM_INFINITY && !(rlim == nil || (rlim.Soft == unix.RLIM_INFINITY && rlim.Hard == unix.RLIM_INFINITY)) { t.Fatalf("wasn't supposed to set limit on number of open files: %#v", rlim) } if limit != unix.RLIM_INFINITY && rlim == nil { t.Fatalf("was supposed to set limit on number of open files") } if rlim != nil { if rlim.Soft != limit { t.Fatalf("soft limit was set to %d, not %d", rlim.Soft, limit) } if rlim.Hard != limit { t.Fatalf("hard limit was set to %d, not %d", rlim.Hard, limit) } } }, ) } } func TestProcessNoNewPrivileges(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } if !seccompAvailable { t.Skip("not built with seccomp support") } for _, nope := range []bool{false, true} { testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.SetProcessNoNewPrivileges(nope) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Process.NoNewPrivileges != nope { t.Fatalf("expected no-new-privs to be %v, got %v", nope, report.Spec.Process.NoNewPrivileges) } }, ) } } func TestProcessOOMScoreAdj(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } for _, adj := range []int{0, 1, 2, 3} { testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.SetProcessOOMScoreAdj(adj) }, func(t *testing.T, report *types.TestReport) { adjusted := 0 if report.Spec.Process.OOMScoreAdj != nil { adjusted = *report.Spec.Process.OOMScoreAdj } if adjusted != adj { t.Fatalf("expected oom-score-adj to be %v, got %v", adj, adjusted) } }, ) } } func TestHostname(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } hostname := fmt.Sprintf("host%d", unix.Getpid()) testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.SetHostname(hostname) }, func(t *testing.T, report *types.TestReport) { if report.Spec.Hostname != hostname { t.Fatalf("expected %q, got %q", hostname, report.Spec.Hostname) } }, ) } func TestMounts(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } t.Run("tmpfs", func(t *testing.T) { testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.AddMount(specs.Mount{ Source: "tmpfs", Destination: "/was-not-there-before", Type: "tmpfs", Options: []string{"ro", "size=0"}, }) }, func(t *testing.T, report *types.TestReport) { found := false for _, mount := range report.Spec.Mounts { if mount.Destination == "/was-not-there-before" && mount.Type == "tmpfs" { found = true } } if !found { t.Errorf("added mount for /was-not-there-before not found in %#v", report.Spec.Mounts) } }, ) }) // apparently we can do anything except turn read-only into read-write binds := []struct { name string tmpfsOptions string destination string fsType string options []string require []string reject []string }{ { name: "nodev", destination: "/nodev", options: []string{"nodev"}, reject: []string{"dev"}, }, { name: "noexec", destination: "/noexec", options: []string{"noexec"}, reject: []string{"exec"}, }, { name: "nosuid", destination: "/nosuid", options: []string{"nosuid"}, reject: []string{"suid"}}, { name: "nodev,noexec", destination: "/nodev,noexec", options: []string{"nodev", "noexec"}, reject: []string{"dev", "exec"}, }, { name: "nodev,noexec,nosuid", destination: "/nodev,noexec,nosuid", options: []string{"nodev", "noexec", "nosuid"}, reject: []string{"dev", "exec", "suid"}, }, { name: "nodev,noexec,nosuid,ro", destination: "/nodev,noexec,nosuid,ro", options: []string{"nodev", "noexec", "nosuid", "ro"}, reject: []string{"dev", "exec", "suid", "rw"}, }, { name: "nodev,noexec,nosuid,rw", destination: "/nodev,noexec,nosuid,rw", options: []string{"nodev", "noexec", "nosuid", "rw"}, reject: []string{"dev", "exec", "suid", "ro"}, }, { name: "dev,exec,suid,rw", tmpfsOptions: "nodev,noexec,nosuid", destination: "/dev,exec,suid,rw", options: []string{"dev", "exec", "suid", "rw"}, require: []string{"rw"}, reject: []string{"nodev", "noexec", "nosuid", "ro"}, }, { name: "nodev,noexec,nosuid,ro,flip", tmpfsOptions: "dev,exec,suid,rw", destination: "/nodev,noexec,nosuid,ro", options: []string{"nodev", "noexec", "nosuid", "ro"}, reject: []string{"dev", "exec", "suid", "rw"}, }, } for _, bind := range binds { t.Run(bind.name, func(t *testing.T) { // mount a tmpfs over the temp dir, which may be on a nodev/noexec/nosuid filesystem tmpfsMount := t.TempDir() t.Cleanup(func() { _ = unix.Unmount(tmpfsMount, unix.MNT_FORCE|unix.MNT_DETACH) }) tmpfsOptions := "rw,size=1m" if bind.tmpfsOptions != "" { tmpfsOptions += ("," + bind.tmpfsOptions) } tmpfsFlags, tmpfsOptions := mount.ParseOptions(tmpfsOptions) require.NoErrorf(t, unix.Mount("none", tmpfsMount, "tmpfs", uintptr(tmpfsFlags), tmpfsOptions), "error mounting a tmpfs with flags=%#x,options=%q at %s", tmpfsFlags, tmpfsOptions, tmpfsMount) testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { fsType := bind.fsType if fsType == "" { fsType = "bind" } g.AddMount(specs.Mount{ Source: tmpfsMount, Destination: bind.destination, Type: fsType, Options: bind.options, }) }, func(t *testing.T, report *types.TestReport) { foundMounts := make(map[string]bool) for _, mount := range report.Spec.Mounts { if mount.Destination == bind.destination { allRequired := true requiredFlags := bind.require if len(requiredFlags) == 0 { requiredFlags = bind.options } for _, required := range requiredFlags { if !util.StringInSlice(required, mount.Options) { allRequired = false } } anyRejected := false for _, rejected := range bind.reject { if util.StringInSlice(rejected, mount.Options) { anyRejected = true } } foundMounts[mount.Destination] = allRequired && !anyRejected } } if !foundMounts[bind.destination] { t.Errorf("added mount for %s not found with the right flags (%v) in %+v", bind.destination, bind.options, report.Spec.Mounts) } }, ) // okay, just make sure we didn't change anything about the tmpfs mount point outside of the chroot var fs unix.Statfs_t require.NoErrorf(t, unix.Statfs(tmpfsMount, &fs), "fstat") assert.Equalf(t, tmpfsFlags&unix.MS_NODEV == unix.MS_NODEV, fs.Flags&unix.ST_NODEV == unix.ST_NODEV, "nodev flag") assert.Equalf(t, tmpfsFlags&unix.MS_NOEXEC == unix.MS_NOEXEC, fs.Flags&unix.ST_NOEXEC == unix.ST_NOEXEC, "noexec flag") assert.Equalf(t, tmpfsFlags&unix.MS_NOSUID == unix.MS_NOSUID, fs.Flags&unix.ST_NOSUID == unix.ST_NOSUID, "nosuid flag") assert.Equalf(t, tmpfsFlags&unix.MS_RDONLY == unix.MS_RDONLY, fs.Flags&unix.ST_RDONLY == unix.ST_RDONLY, "readonly flag") }) } } func TestLinuxIDMapping(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.ClearLinuxUIDMappings() g.ClearLinuxGIDMappings() g.AddLinuxUIDMapping(uint32(unix.Getuid()), 0, 1) g.AddLinuxGIDMapping(uint32(unix.Getgid()), 0, 1) }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Linux.UIDMappings) != 1 { t.Fatalf("expected 1 uid mapping, got %q", len(report.Spec.Linux.UIDMappings)) } if report.Spec.Linux.UIDMappings[0].HostID != uint32(unix.Getuid()) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getuid(), report.Spec.Linux.UIDMappings[0].HostID) } if report.Spec.Linux.UIDMappings[0].ContainerID != 0 { t.Fatalf("expected container uid mapping to be 0, got %d", report.Spec.Linux.UIDMappings[0].ContainerID) } if report.Spec.Linux.UIDMappings[0].Size != 1 { t.Fatalf("expected container uid map size to be 1, got %d", report.Spec.Linux.UIDMappings[0].Size) } if report.Spec.Linux.GIDMappings[0].HostID != uint32(unix.Getgid()) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getgid(), report.Spec.Linux.GIDMappings[0].HostID) } if report.Spec.Linux.GIDMappings[0].ContainerID != 0 { t.Fatalf("expected container gid mapping to be 0, got %d", report.Spec.Linux.GIDMappings[0].ContainerID) } if report.Spec.Linux.GIDMappings[0].Size != 1 { t.Fatalf("expected container gid map size to be 1, got %d", report.Spec.Linux.GIDMappings[0].Size) } }, ) } func TestLinuxIDMappingShift(t *testing.T) { if unix.Getuid() != 0 { t.Skip("tests need to be run as root") } testMinimal(t, func(g *generate.Generator, rootDir, bundleDir string) { g.ClearLinuxUIDMappings() g.ClearLinuxGIDMappings() g.AddLinuxUIDMapping(uint32(unix.Getuid())+1, 0, 1) g.AddLinuxGIDMapping(uint32(unix.Getgid())+1, 0, 1) }, func(t *testing.T, report *types.TestReport) { if len(report.Spec.Linux.UIDMappings) != 1 { t.Fatalf("expected 1 uid mapping, got %q", len(report.Spec.Linux.UIDMappings)) } if report.Spec.Linux.UIDMappings[0].HostID != uint32(unix.Getuid()+1) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getuid()+1, report.Spec.Linux.UIDMappings[0].HostID) } if report.Spec.Linux.UIDMappings[0].ContainerID != 0 { t.Fatalf("expected container uid mapping to be 0, got %d", report.Spec.Linux.UIDMappings[0].ContainerID) } if report.Spec.Linux.UIDMappings[0].Size != 1 { t.Fatalf("expected container uid map size to be 1, got %d", report.Spec.Linux.UIDMappings[0].Size) } if report.Spec.Linux.GIDMappings[0].HostID != uint32(unix.Getgid()+1) { t.Fatalf("expected host uid mapping to be %d, got %d", unix.Getgid()+1, report.Spec.Linux.GIDMappings[0].HostID) } if report.Spec.Linux.GIDMappings[0].ContainerID != 0 { t.Fatalf("expected container gid mapping to be 0, got %d", report.Spec.Linux.GIDMappings[0].ContainerID) } if report.Spec.Linux.GIDMappings[0].Size != 1 { t.Fatalf("expected container gid map size to be 1, got %d", report.Spec.Linux.GIDMappings[0].Size) } }, ) }