@@ -18,16 +18,21 @@ package validate
1818
1919import (
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 (
3843const (
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
4350var _ = 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