Skip to content

Commit 4f2ddb3

Browse files
committed
userns: Skip tests if the host doesn't support idmap mounts
critest is used in projects like containerd, that test against older distros (like AlmaLinux 8). In those distros, CI will fail when we upgrade to runc 1.2.0. With runc 1.1 those test don't fail because runc doesn't support idmap mounts and the tests are skipped in that case. But with runc 1.2.0-rc.2, that supports idmap mounts, the tests are not skipped but fail on distros with older kernels that don't support idmap mounts. This commit just tries to detect if the path used for the container rootfs supports idmap mounts. To do that it uses the Status() message from CRI with verbose param set to true. It parses the output that containerd sets (it's quite unspecified that field), and otherwise fallbacks to "/var/lib" as the path to test idmap mounts support. Signed-off-by: Rodrigo Campos <rodrigoca@microsoft.com>
1 parent 71030a7 commit 4f2ddb3

File tree

1 file changed

+80
-1
lines changed

1 file changed

+80
-1
lines changed

pkg/validate/security_context_linux.go

+80-1
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ package validate
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"net"
2324
"os"
2425
"os/exec"
2526
"path/filepath"
2627
"strings"
28+
"syscall"
2729
"time"
2830

31+
"golang.org/x/sys/unix"
2932
internalapi "k8s.io/cri-api/pkg/apis"
3033
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
3134
"sigs.k8s.io/cri-tools/pkg/common"
@@ -860,7 +863,7 @@ var _ = framework.KubeDescribe("Security Context", func() {
860863
By("searching for runtime handler which supports user namespaces")
861864
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
862865
defer cancel()
863-
resp, err := rc.Status(ctx, false)
866+
resp, err := rc.Status(ctx, true) // Set verbose to true so the info field is populated.
864867
framework.ExpectNoError(err, "failed to get runtime config: %v", err)
865868

866869
supportsUserNamespaces := false
@@ -876,6 +879,11 @@ var _ = framework.KubeDescribe("Security Context", func() {
876879
if !supportsUserNamespaces {
877880
Skip("no runtime handler found which supports user namespaces")
878881
}
882+
883+
pathIDMap := rootfsPath(resp.GetInfo())
884+
if err := supportsIDMap(pathIDMap); err != nil {
885+
Skip("ID mapping is not supported" + " with path: " + pathIDMap + ": " + err.Error())
886+
}
879887
})
880888

881889
It("runtime should support NamespaceMode_POD", func() {
@@ -1458,3 +1466,74 @@ func runUserNamespacePodWithError(
14581466

14591467
framework.RunPodSandboxError(rc, config)
14601468
}
1469+
1470+
func supportsIDMap(path string) error {
1471+
treeFD, err := unix.OpenTree(-1, path, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC))
1472+
if err != nil {
1473+
return err
1474+
}
1475+
defer unix.Close(treeFD)
1476+
1477+
// We want to test if idmap mounts are supported.
1478+
// So we use just some random mapping, it doesn't really matter which one.
1479+
// For the helper command, we just need something that is alive while we
1480+
// test this, a sleep 5 will do it.
1481+
cmd := exec.Command("sleep", "5")
1482+
cmd.SysProcAttr = &syscall.SysProcAttr{
1483+
Cloneflags: syscall.CLONE_NEWUSER,
1484+
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}},
1485+
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}},
1486+
}
1487+
if err := cmd.Start(); err != nil {
1488+
return err
1489+
}
1490+
defer func() {
1491+
_ = cmd.Process.Kill()
1492+
_ = cmd.Wait()
1493+
}()
1494+
1495+
usernsPath := fmt.Sprintf("/proc/%d/ns/user", cmd.Process.Pid)
1496+
var usernsFile *os.File
1497+
if usernsFile, err = os.Open(usernsPath); err != nil {
1498+
return err
1499+
}
1500+
defer usernsFile.Close()
1501+
1502+
attr := unix.MountAttr{
1503+
Attr_set: unix.MOUNT_ATTR_IDMAP,
1504+
Userns_fd: uint64(usernsFile.Fd()),
1505+
}
1506+
if err := unix.MountSetattr(treeFD, "", unix.AT_EMPTY_PATH, &attr); err != nil {
1507+
return err
1508+
}
1509+
1510+
return nil
1511+
}
1512+
1513+
// rootfsPath returns the parent path used for containerd stateDir (the container rootfs lives
1514+
// inside there). If the object can't be parsed, it returns the "/var/lib".
1515+
// Usually the rootfs is inside /var/lib and it's the same filesystem. In the end, to see if a path
1516+
// supports idmap, we only care about its fs so this is a good fallback.
1517+
func rootfsPath(info map[string]string) string {
1518+
defaultPath := "/var/lib"
1519+
jsonCfg, ok := info["config"]
1520+
if !ok {
1521+
return defaultPath
1522+
}
1523+
1524+
// Get only the StateDir from the json.
1525+
type containerdConfig struct {
1526+
StateDir string `json:"stateDir"`
1527+
}
1528+
cfg := containerdConfig{}
1529+
if err := json.Unmarshal([]byte(jsonCfg), &cfg); err != nil {
1530+
return defaultPath
1531+
}
1532+
if cfg.StateDir == "" {
1533+
return defaultPath
1534+
}
1535+
1536+
// The stateDir might have not been created yet. Let's use the parent directory that should
1537+
// always exist.
1538+
return filepath.Join(cfg.StateDir, "../")
1539+
}

0 commit comments

Comments
 (0)