Skip to content

Commit b633b38

Browse files
authored
Merge pull request #1492 from saschagrunert/release-1.30-userns
[release-1.30] userns: Skip tests if the host doesn't support idmap mounts
2 parents 87adf58 + 22e6548 commit b633b38

File tree

2 files changed

+217
-73
lines changed

2 files changed

+217
-73
lines changed

Diff for: .github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171

7272
strategy:
7373
matrix:
74-
os: [ubuntu-22.04, macos-11] # TODO: make releases on 'linux-ppc64le' 'windows-2019'
74+
os: [ubuntu-22.04, macos-12] # TODO: make releases on 'linux-ppc64le' 'windows-2019'
7575
# Ref: https://github.com/uraimo/run-on-arch-action
7676
steps:
7777
- name: Install Go

Diff for: pkg/validate/security_context_linux.go

+216-72
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,21 @@ package validate
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"net"
2324
"os"
2425
"os/exec"
2526
"path/filepath"
27+
"slices"
2628
"strings"
29+
"sync"
30+
"syscall"
2731
"time"
2832

2933
"github.com/kubernetes-sigs/cri-tools/pkg/common"
3034
"github.com/kubernetes-sigs/cri-tools/pkg/framework"
35+
"golang.org/x/sys/unix"
3136
internalapi "k8s.io/cri-api/pkg/apis"
3237
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
3338

@@ -38,6 +43,8 @@ import (
3843
const (
3944
nginxContainerImage string = framework.DefaultRegistryE2ETestImagesPrefix + "nginx:1.14-2"
4045
noNewPrivsImage string = framework.DefaultRegistryE2ETestImagesPrefix + "nonewprivs:1.3"
46+
usernsSize int = 65536
47+
usernsHostID int = 65536
4148
)
4249

4350
var _ = framework.KubeDescribe("Security Context", func() {
@@ -845,7 +852,12 @@ var _ = framework.KubeDescribe("Security Context", func() {
845852

846853
Context("UserNamespaces", func() {
847854
var (
848-
podName string
855+
podName string
856+
857+
// We call rc.Status() once and save the result in statusResp.
858+
statusOnce sync.Once
859+
statusResp *runtimeapi.StatusResponse
860+
849861
defaultMapping = []*runtimeapi.IDMapping{{
850862
ContainerId: 0,
851863
HostId: 1000,
@@ -858,103 +870,143 @@ var _ = framework.KubeDescribe("Security Context", func() {
858870

859871
// Find a working runtime handler if none provided
860872
By("searching for runtime handler which supports user namespaces")
861-
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
862-
defer cancel()
863-
resp, err := rc.Status(ctx, false)
864-
framework.ExpectNoError(err, "failed to get runtime config: %v", err)
865-
866-
supportsUserNamespaces := false
867-
for _, rh := range resp.GetRuntimeHandlers() {
873+
statusOnce.Do(func() {
874+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
875+
defer cancel()
876+
// Set verbose to true, other BeforeEachs calls need the info field
877+
// populated.
878+
// XXX: Do NOT use ":=" here, it breaks the closure reference to
879+
// statusResp.
880+
var err error
881+
statusResp, err = rc.Status(ctx, true)
882+
framework.ExpectNoError(err, "failed to get runtime config: %v", err)
883+
_ = statusResp // Avoid unused variable error
884+
})
885+
886+
var supportsUserNamespaces bool
887+
for _, rh := range statusResp.GetRuntimeHandlers() {
868888
if rh.GetName() == framework.TestContext.RuntimeHandler {
869889
if rh.GetFeatures().GetUserNamespaces() {
870890
supportsUserNamespaces = true
871891
break
872892
}
873893
}
874894
}
875-
876895
if !supportsUserNamespaces {
877896
Skip("no runtime handler found which supports user namespaces")
878897
}
879898
})
880899

881-
It("runtime should support NamespaceMode_POD", func() {
882-
namespaceOption := &runtimeapi.NamespaceOption{
883-
UsernsOptions: &runtimeapi.UserNamespace{
884-
Mode: runtimeapi.NamespaceMode_POD,
885-
Uids: defaultMapping,
886-
Gids: defaultMapping,
887-
},
888-
}
900+
When("Host idmap mount support is needed", func() {
901+
BeforeEach(func() {
902+
pathIDMap := rootfsPath(statusResp.GetInfo())
903+
if err := supportsIDMap(pathIDMap); err != nil {
904+
Skip("ID mapping is not supported" + " with path: " + pathIDMap + ": " + err.Error())
905+
}
906+
})
907+
908+
It("runtime should support NamespaceMode_POD", func() {
909+
namespaceOption := &runtimeapi.NamespaceOption{
910+
UsernsOptions: &runtimeapi.UserNamespace{
911+
Mode: runtimeapi.NamespaceMode_POD,
912+
Uids: defaultMapping,
913+
Gids: defaultMapping,
914+
},
915+
}
889916

890-
hostLogPath, podLogPath := createLogTempDir(podName)
891-
defer os.RemoveAll(hostLogPath)
892-
podID, podConfig = createNamespacePodSandbox(rc, namespaceOption, podName, podLogPath)
893-
containerName := runUserNamespaceContainer(rc, ic, podID, podConfig)
917+
hostLogPath, podLogPath := createLogTempDir(podName)
918+
defer os.RemoveAll(hostLogPath)
919+
podID, podConfig = createNamespacePodSandbox(rc, namespaceOption, podName, podLogPath)
920+
containerName := runUserNamespaceContainer(rc, ic, podID, podConfig)
921+
922+
matchContainerOutputRe(podConfig, containerName, `\s+0\s+1000\s+100000\n`)
923+
})
894924

895-
matchContainerOutputRe(podConfig, containerName, `\s+0\s+1000\s+100000\n`)
896925
})
897926

898-
It("runtime should support NamespaceMode_NODE", func() {
899-
namespaceOption := &runtimeapi.NamespaceOption{
900-
UsernsOptions: &runtimeapi.UserNamespace{
901-
Mode: runtimeapi.NamespaceMode_NODE,
902-
},
903-
}
927+
When("Host idmap mount support is not needed", func() {
928+
It("runtime should support NamespaceMode_NODE", func() {
929+
namespaceOption := &runtimeapi.NamespaceOption{
930+
UsernsOptions: &runtimeapi.UserNamespace{
931+
Mode: runtimeapi.NamespaceMode_NODE,
932+
},
933+
}
904934

905-
hostLogPath, podLogPath := createLogTempDir(podName)
906-
defer os.RemoveAll(hostLogPath)
907-
podID, podConfig = createNamespacePodSandbox(rc, namespaceOption, podName, podLogPath)
908-
containerName := runUserNamespaceContainer(rc, ic, podID, podConfig)
935+
hostLogPath, podLogPath := createLogTempDir(podName)
936+
defer os.RemoveAll(hostLogPath)
937+
podID, podConfig = createNamespacePodSandbox(rc, namespaceOption, podName, podLogPath)
938+
containerName := runUserNamespaceContainer(rc, ic, podID, podConfig)
909939

910-
// 4294967295 means that the entire range is available
911-
matchContainerOutputRe(podConfig, containerName, `\s+0\s+0\s+4294967295\n`)
912-
})
940+
// If this test is run inside a userns, we need to check the
941+
// container userns is the same as the one we see outside.
942+
expectedOutput := hostUsernsContent()
943+
if expectedOutput == "" {
944+
Fail("failed to get host userns content")
945+
}
946+
// The userns mapping can have several lines, we match each of them.
947+
for _, line := range strings.Split(expectedOutput, "\n") {
948+
if line == "" {
949+
continue
950+
}
951+
mapping := parseUsernsMappingLine(line)
952+
if len(mapping) != 3 {
953+
msg := fmt.Sprintf("slice: %#v, len: %v", mapping, len(mapping))
954+
Fail("Unexpected host mapping line: " + msg)
955+
}
913956

914-
It("runtime should fail if more than one mapping provided", func() {
915-
wrongMapping := []*runtimeapi.IDMapping{{
916-
ContainerId: 0,
917-
HostId: 1000,
918-
Length: 100000,
919-
}, {
920-
ContainerId: 0,
921-
HostId: 2000,
922-
Length: 100000,
923-
}}
924-
usernsOptions := &runtimeapi.UserNamespace{
925-
Mode: runtimeapi.NamespaceMode_POD,
926-
Uids: wrongMapping,
927-
Gids: wrongMapping,
928-
}
957+
// The container outputs the content of its /proc/self/uid_map.
958+
// That output should match the regex of the host userns content.
959+
containerId, hostId, length := mapping[0], mapping[1], mapping[2]
960+
regex := fmt.Sprintf(`\s+%v\s+%v\s+%v`, containerId, hostId, length)
961+
matchContainerOutputRe(podConfig, containerName, regex)
962+
}
963+
})
964+
965+
It("runtime should fail if more than one mapping provided", func() {
966+
wrongMapping := []*runtimeapi.IDMapping{{
967+
ContainerId: 0,
968+
HostId: 1000,
969+
Length: 100000,
970+
}, {
971+
ContainerId: 0,
972+
HostId: 2000,
973+
Length: 100000,
974+
}}
975+
usernsOptions := &runtimeapi.UserNamespace{
976+
Mode: runtimeapi.NamespaceMode_POD,
977+
Uids: wrongMapping,
978+
Gids: wrongMapping,
979+
}
929980

930-
runUserNamespacePodWithError(rc, podName, usernsOptions)
931-
})
981+
runUserNamespacePodWithError(rc, podName, usernsOptions)
982+
})
932983

933-
It("runtime should fail if container ID 0 is not mapped", func() {
934-
mapping := []*runtimeapi.IDMapping{{
935-
ContainerId: 1,
936-
HostId: 1000,
937-
Length: 100000,
938-
}}
939-
usernsOptions := &runtimeapi.UserNamespace{
940-
Mode: runtimeapi.NamespaceMode_POD,
941-
Uids: mapping,
942-
Gids: mapping,
943-
}
984+
It("runtime should fail if container ID 0 is not mapped", func() {
985+
mapping := []*runtimeapi.IDMapping{{
986+
ContainerId: 1,
987+
HostId: 1000,
988+
Length: 100000,
989+
}}
990+
usernsOptions := &runtimeapi.UserNamespace{
991+
Mode: runtimeapi.NamespaceMode_POD,
992+
Uids: mapping,
993+
Gids: mapping,
994+
}
944995

945-
runUserNamespacePodWithError(rc, podName, usernsOptions)
946-
})
996+
runUserNamespacePodWithError(rc, podName, usernsOptions)
997+
})
947998

948-
It("runtime should fail with NamespaceMode_CONTAINER", func() {
949-
usernsOptions := &runtimeapi.UserNamespace{Mode: runtimeapi.NamespaceMode_CONTAINER}
999+
It("runtime should fail with NamespaceMode_CONTAINER", func() {
1000+
usernsOptions := &runtimeapi.UserNamespace{Mode: runtimeapi.NamespaceMode_CONTAINER}
9501001

951-
runUserNamespacePodWithError(rc, podName, usernsOptions)
952-
})
1002+
runUserNamespacePodWithError(rc, podName, usernsOptions)
1003+
})
9531004

954-
It("runtime should fail with NamespaceMode_TARGET", func() {
955-
usernsOptions := &runtimeapi.UserNamespace{Mode: runtimeapi.NamespaceMode_TARGET}
1005+
It("runtime should fail with NamespaceMode_TARGET", func() {
1006+
usernsOptions := &runtimeapi.UserNamespace{Mode: runtimeapi.NamespaceMode_TARGET}
9561007

957-
runUserNamespacePodWithError(rc, podName, usernsOptions)
1008+
runUserNamespacePodWithError(rc, podName, usernsOptions)
1009+
})
9581010
})
9591011
})
9601012
})
@@ -1458,3 +1510,95 @@ func runUserNamespacePodWithError(
14581510

14591511
framework.RunPodSandboxError(rc, config)
14601512
}
1513+
1514+
func supportsIDMap(path string) error {
1515+
treeFD, err := unix.OpenTree(-1, path, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC))
1516+
if err != nil {
1517+
return err
1518+
}
1519+
defer unix.Close(treeFD)
1520+
1521+
// We want to test if idmap mounts are supported.
1522+
// So we use just some random mapping, it doesn't really matter which one.
1523+
// For the helper command, we just need something that is alive while we
1524+
// test this, a sleep 5 will do it.
1525+
cmd := exec.Command("sleep", "5")
1526+
cmd.SysProcAttr = &syscall.SysProcAttr{
1527+
Cloneflags: syscall.CLONE_NEWUSER,
1528+
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: usernsHostID, Size: usernsSize}},
1529+
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: usernsHostID, Size: usernsSize}},
1530+
}
1531+
if err := cmd.Start(); err != nil {
1532+
return err
1533+
}
1534+
defer func() {
1535+
_ = cmd.Process.Kill()
1536+
_ = cmd.Wait()
1537+
}()
1538+
1539+
usernsPath := fmt.Sprintf("/proc/%d/ns/user", cmd.Process.Pid)
1540+
var usernsFile *os.File
1541+
if usernsFile, err = os.Open(usernsPath); err != nil {
1542+
return err
1543+
}
1544+
defer usernsFile.Close()
1545+
1546+
attr := unix.MountAttr{
1547+
Attr_set: unix.MOUNT_ATTR_IDMAP,
1548+
Userns_fd: uint64(usernsFile.Fd()),
1549+
}
1550+
if err := unix.MountSetattr(treeFD, "", unix.AT_EMPTY_PATH, &attr); err != nil {
1551+
return err
1552+
}
1553+
1554+
return nil
1555+
}
1556+
1557+
// rootfsPath returns the parent path used for containerd stateDir (the container rootfs lives
1558+
// inside there). If the object can't be parsed, it returns the "/var/lib".
1559+
// Usually the rootfs is inside /var/lib and it's the same filesystem. In the end, to see if a path
1560+
// supports idmap, we only care about its fs so this is a good fallback.
1561+
func rootfsPath(info map[string]string) string {
1562+
defaultPath := "/var/lib"
1563+
jsonCfg, ok := info["config"]
1564+
if !ok {
1565+
return defaultPath
1566+
}
1567+
1568+
// Get only the StateDir from the json.
1569+
type containerdConfig struct {
1570+
StateDir string `json:"stateDir"`
1571+
}
1572+
cfg := containerdConfig{}
1573+
if err := json.Unmarshal([]byte(jsonCfg), &cfg); err != nil {
1574+
return defaultPath
1575+
}
1576+
if cfg.StateDir == "" {
1577+
return defaultPath
1578+
}
1579+
1580+
// The stateDir might have not been created yet. Let's use the parent directory that should
1581+
// always exist.
1582+
return filepath.Join(cfg.StateDir, "../")
1583+
}
1584+
1585+
func hostUsernsContent() string {
1586+
uidMapPath := "/proc/self/uid_map"
1587+
uidMapContent, err := os.ReadFile(uidMapPath)
1588+
if err != nil {
1589+
return ""
1590+
}
1591+
return string(uidMapContent)
1592+
}
1593+
1594+
func parseUsernsMappingLine(line string) []string {
1595+
// The line format is:
1596+
// <container-id> <host-id> <length>
1597+
// But there could be a lot of spaces between the fields.
1598+
line = strings.TrimSpace(line)
1599+
m := strings.Split(line, " ")
1600+
m = slices.DeleteFunc(m, func(s string) bool {
1601+
return s == ""
1602+
})
1603+
return m
1604+
}

0 commit comments

Comments
 (0)