1
0
mirror of https://github.com/containers/buildah.git synced 2025-09-17 15:42:18 +03:00
Files
buildah/chroot/run_test.go
2025-08-29 15:04:28 +02:00

635 lines
19 KiB
Go

//go:build linux
package chroot
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"testing"
"github.com/containers/buildah/tests/testreport/types"
"github.com/containers/buildah/util"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.podman.io/storage/pkg/mount"
"go.podman.io/storage/pkg/reexec"
"golang.org/x/sys/unix"
)
const (
reportCommand = "testreport"
)
func TestMain(m *testing.M) {
if reexec.Init() {
return
}
os.Exit(m.Run())
}
func testMinimalWithPivot(t *testing.T, noPivot bool, 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), 0o711); err != nil {
t.Fatalf("error loosening permissions on %q: %v", tempDir, err)
}
rootDir := filepath.Join(tempDir, "root")
if err := os.Mkdir(rootDir, 0o711); err != nil {
t.Fatalf("os.Mkdir(%q): %v", rootDir, err)
}
rootTmpDir := filepath.Join(rootDir, "tmp")
if err := os.Mkdir(rootTmpDir, 0o1777); 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, 0o711)
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, 0o700); 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, noPivot); err != nil {
t.Fatalf("run(noPivot=%v): %v: %s", noPivot, 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 testMinimal(t *testing.T, modify func(g *generate.Generator, rootDir, bundleDir string), verify func(t *testing.T, report *types.TestReport)) {
for _, noPivot := range []bool{false, true} {
t.Run(fmt.Sprintf("noPivot=%v", noPivot), func(t *testing.T) {
testMinimalWithPivot(t, noPivot, modify, verify)
})
}
}
func TestNoop(t *testing.T) {
t.Parallel()
if unix.Getuid() != 0 {
t.Skip("tests need to be run as root")
}
testMinimal(t, nil, nil)
}
func TestMinimalSkeleton(t *testing.T) {
t.Parallel()
if unix.Getuid() != 0 {
t.Skip("tests need to be run as root")
}
testMinimal(t,
func(_ *generate.Generator, _, _ string) {
},
func(_ *testing.T, _ *types.TestReport) {
},
)
}
func TestProcessTerminal(t *testing.T) {
t.Parallel()
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, _, _ 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) {
t.Parallel()
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, _, _ 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) {
t.Parallel()
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, _, _ 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) {
t.Parallel()
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, _, _ string) {
g.ClearProcessEnv()
g.AddProcessEnv("PARENT_TEST_PID", strconv.Itoa(unix.Getpid()))
},
func(t *testing.T, report *types.TestReport) {
if slices.Contains(report.Spec.Process.Env, e) {
return
}
t.Fatalf("expected environment variable %q", e)
},
)
}
func TestProcessCwd(t *testing.T) {
t.Parallel()
if unix.Getuid() != 0 {
t.Skip("tests need to be run as root")
}
testMinimal(t,
func(g *generate.Generator, rootDir, _ string) {
if err := os.Mkdir(filepath.Join(rootDir, "/no-such-directory"), 0o700); 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) {
t.Parallel()
if unix.Getuid() != 0 {
t.Skip("tests need to be run as root")
}
testMinimal(t,
func(g *generate.Generator, _, _ 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, _, _ 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) {
t.Parallel()
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, _, _ 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) {
t.Parallel()
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, _, _ 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) {
t.Parallel()
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, _, _ 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) {
t.Parallel()
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, _, _ 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) {
t.Parallel()
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, _, _ 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, _, _ 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) {
foundBindDestinationMount := false
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 !slices.Contains(mount.Options, required) {
allRequired = false
}
}
anyRejected := false
for _, rejected := range bind.reject {
if slices.Contains(mount.Options, rejected) {
anyRejected = true
}
}
if allRequired && !anyRejected {
foundBindDestinationMount = true
}
}
}
if !foundBindDestinationMount {
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) {
t.Parallel()
if unix.Getuid() != 0 {
t.Skip("tests need to be run as root")
}
testMinimal(t,
func(g *generate.Generator, _, _ 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) {
t.Parallel()
if unix.Getuid() != 0 {
t.Skip("tests need to be run as root")
}
testMinimal(t,
func(g *generate.Generator, _, _ 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)
}
},
)
}