diff --git a/integration/main_test.go b/integration/main_test.go index 24f6ac93b..4c9e733ae 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -298,6 +298,21 @@ func WithVolumeMount(hostPath, containerPath string) ContainerOpts { } } +func WithIDMapVolumeMount(hostPath, containerPath string, uidMaps, gidMaps []*runtime.IDMapping) ContainerOpts { + return func(c *runtime.ContainerConfig) { + hostPath, _ = filepath.Abs(hostPath) + containerPath, _ = filepath.Abs(containerPath) + mount := &runtime.Mount{ + HostPath: hostPath, + ContainerPath: containerPath, + SelinuxRelabel: selinux.GetEnabled(), + UidMappings: uidMaps, + GidMappings: gidMaps, + } + c.Mounts = append(c.Mounts, mount) + } +} + func WithWindowsUsername(username string) ContainerOpts { return func(c *runtime.ContainerConfig) { if c.Windows == nil { diff --git a/integration/pod_userns_linux_test.go b/integration/pod_userns_linux_test.go index e0a561433..9553a48cc 100644 --- a/integration/pod_userns_linux_test.go +++ b/integration/pod_userns_linux_test.go @@ -19,7 +19,9 @@ package integration import ( "fmt" "os" + "os/user" "path/filepath" + "strings" "syscall" "testing" "time" @@ -28,17 +30,121 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" exec "golang.org/x/sys/execabs" + "golang.org/x/sys/unix" runtime "k8s.io/cri-api/pkg/apis/runtime/v1" ) +const ( + defaultRoot = "/var/lib/containerd-test" +) + +func supportsUserNS() bool { + if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { + return false + } + return true +} + +func supportsIDMap(path string) bool { + treeFD, err := unix.OpenTree(-1, path, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC)) + if err != nil { + return false + } + defer unix.Close(treeFD) + + // We want to test if idmap mounts are supported. + // So we use just some random mapping, it doesn't really matter which one. + // For the helper command, we just need something that is alive while we + // test this, a sleep 5 will do it. + cmd := exec.Command("sleep", "5") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWUSER, + UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}}, + GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}}, + } + if err := cmd.Start(); err != nil { + return false + } + defer func() { + _ = cmd.Process.Kill() + _ = cmd.Wait() + }() + + usernsFD := fmt.Sprintf("/proc/%d/ns/user", cmd.Process.Pid) + var usernsFile *os.File + if usernsFile, err = os.Open(usernsFD); err != nil { + return false + } + defer usernsFile.Close() + + attr := unix.MountAttr{ + Attr_set: unix.MOUNT_ATTR_IDMAP, + Userns_fd: uint64(usernsFile.Fd()), + } + if err := unix.MountSetattr(treeFD, "", unix.AT_EMPTY_PATH, &attr); err != nil { + return false + } + + return true +} + +// traversePath gives 755 permissions for all elements in tPath below +// os.TempDir() and errors out if elements above it don't have read+exec +// permissions for others. tPath MUST be a descendant of os.TempDir(). The path +// returned by testing.TempDir() usually is. +func traversePath(tPath string) error { + // Check the assumption that the argument is under os.TempDir(). + tempBase := os.TempDir() + if !strings.HasPrefix(tPath, tempBase) { + return fmt.Errorf("traversePath: %q is not a descendant of %q", tPath, tempBase) + } + + var path string + for _, p := range strings.SplitAfter(tPath, "/") { + path = path + p + stats, err := os.Stat(path) + if err != nil { + return err + } + + perm := stats.Mode().Perm() + if perm&0o5 == 0o5 { + continue + } + if strings.HasPrefix(tempBase, path) { + return fmt.Errorf("traversePath: directory %q MUST have read+exec permissions for others", path) + } + + if err := os.Chmod(path, perm|0o755); err != nil { + return err + } + } + + return nil +} + func TestPodUserNS(t *testing.T) { containerID := uint32(0) hostID := uint32(65536) size := uint32(65536) + idmap := []*runtime.IDMapping{ + { + ContainerId: containerID, + HostId: hostID, + Length: size, + }, + } + + volumeHostPath := t.TempDir() + if err := traversePath(volumeHostPath); err != nil { + t.Fatalf("failed to setup volume host path: %v", err) + } + for name, test := range map[string]struct { sandboxOpts []PodSandboxOpts containerOpts []ContainerOpts checkOutput func(t *testing.T, output string) + hostVolumes bool // whether to config uses host Volumes expectErr bool }{ "userns uid mapping": { @@ -85,6 +191,31 @@ func TestPodUserNS(t *testing.T) { assert.Contains(t, output, "=0=0=") }, }, + "volumes permissions": { + sandboxOpts: []PodSandboxOpts{ + WithPodUserNs(containerID, hostID, size), + }, + hostVolumes: true, + containerOpts: []ContainerOpts{ + WithUserNamespace(containerID, hostID, size), + WithIDMapVolumeMount(volumeHostPath, "/mnt", idmap, idmap), + // Prints numeric UID and GID for path. + // For example, if UID and GID is 0 it will print: =0=0= + // We add the "=" signs so we use can assert.Contains() and be sure + // the UID/GID is 0 and not things like 100 (that contain 0). + // We can't use assert.Equal() easily as it contains timestamp, etc. + WithCommand("stat", "-c", "'=%u=%g='", "/mnt/"), + }, + checkOutput: func(t *testing.T, output string) { + // The UID and GID should be the current user if chown/remap is done correctly. + uid := "0" + user, err := user.Current() + if user != nil && err == nil { + uid = user.Uid + } + assert.Contains(t, output, "="+uid+"="+uid+"=") + }, + }, "fails with several mappings": { sandboxOpts: []PodSandboxOpts{ WithPodUserNs(containerID, hostID, size), @@ -94,12 +225,14 @@ func TestPodUserNS(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - cmd := exec.Command("true") - cmd.SysProcAttr = &syscall.SysProcAttr{ - Cloneflags: syscall.CLONE_NEWUSER, + if !supportsUserNS() { + t.Skip("User namespaces are not supported") } - if err := cmd.Run(); err != nil { - t.Skip("skipping test: user namespaces are unavailable") + if !supportsIDMap(defaultRoot) { + t.Skipf("ID mappings are not supported on: %v", defaultRoot) + } + if test.hostVolumes && !supportsIDMap(volumeHostPath) { + t.Skipf("ID mappings are not supported host volume filesystem: %v", volumeHostPath) } testPodLogDir := t.TempDir()