@@ -18,16 +18,21 @@ package validate
18
18
19
19
import (
20
20
"context"
21
+ "encoding/json"
21
22
"fmt"
22
23
"net"
23
24
"os"
24
25
"os/exec"
25
26
"path/filepath"
27
+ "slices"
26
28
"strings"
29
+ "sync"
30
+ "syscall"
27
31
"time"
28
32
29
33
"github.com/kubernetes-sigs/cri-tools/pkg/common"
30
34
"github.com/kubernetes-sigs/cri-tools/pkg/framework"
35
+ "golang.org/x/sys/unix"
31
36
internalapi "k8s.io/cri-api/pkg/apis"
32
37
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
33
38
@@ -38,6 +43,8 @@ import (
38
43
const (
39
44
nginxContainerImage string = framework .DefaultRegistryE2ETestImagesPrefix + "nginx:1.14-2"
40
45
noNewPrivsImage string = framework .DefaultRegistryE2ETestImagesPrefix + "nonewprivs:1.3"
46
+ usernsSize int = 65536
47
+ usernsHostID int = 65536
41
48
)
42
49
43
50
var _ = framework .KubeDescribe ("Security Context" , func () {
@@ -845,7 +852,12 @@ var _ = framework.KubeDescribe("Security Context", func() {
845
852
846
853
Context ("UserNamespaces" , func () {
847
854
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
+
849
861
defaultMapping = []* runtimeapi.IDMapping {{
850
862
ContainerId : 0 ,
851
863
HostId : 1000 ,
@@ -858,103 +870,143 @@ var _ = framework.KubeDescribe("Security Context", func() {
858
870
859
871
// Find a working runtime handler if none provided
860
872
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 () {
868
888
if rh .GetName () == framework .TestContext .RuntimeHandler {
869
889
if rh .GetFeatures ().GetUserNamespaces () {
870
890
supportsUserNamespaces = true
871
891
break
872
892
}
873
893
}
874
894
}
875
-
876
895
if ! supportsUserNamespaces {
877
896
Skip ("no runtime handler found which supports user namespaces" )
878
897
}
879
898
})
880
899
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
+ }
889
916
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
+ })
894
924
895
- matchContainerOutputRe (podConfig , containerName , `\s+0\s+1000\s+100000\n` )
896
925
})
897
926
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
+ }
904
934
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 )
909
939
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
+ }
913
956
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
+ }
929
980
930
- runUserNamespacePodWithError (rc , podName , usernsOptions )
931
- })
981
+ runUserNamespacePodWithError (rc , podName , usernsOptions )
982
+ })
932
983
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
+ }
944
995
945
- runUserNamespacePodWithError (rc , podName , usernsOptions )
946
- })
996
+ runUserNamespacePodWithError (rc , podName , usernsOptions )
997
+ })
947
998
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 }
950
1001
951
- runUserNamespacePodWithError (rc , podName , usernsOptions )
952
- })
1002
+ runUserNamespacePodWithError (rc , podName , usernsOptions )
1003
+ })
953
1004
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 }
956
1007
957
- runUserNamespacePodWithError (rc , podName , usernsOptions )
1008
+ runUserNamespacePodWithError (rc , podName , usernsOptions )
1009
+ })
958
1010
})
959
1011
})
960
1012
})
@@ -1458,3 +1510,95 @@ func runUserNamespacePodWithError(
1458
1510
1459
1511
framework .RunPodSandboxError (rc , config )
1460
1512
}
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