diff --git a/VERSION b/VERSION index 9631642597d..81f32601221 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.11-rc1.m1 +1.1.12-m1 diff --git a/go.mod b/go.mod index ecee68d89a5..172f2085356 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/cilium/ebpf v0.7.0 github.com/containerd/console v1.0.3 github.com/coreos/go-systemd/v22 v22.3.2 - github.com/cyphar/filepath-securejoin v0.2.3 + github.com/cyphar/filepath-securejoin v0.2.4 github.com/docker/go-units v0.4.0 github.com/godbus/dbus/v5 v5.0.6 github.com/moby/sys/mountinfo v0.5.0 diff --git a/go.sum b/go.sum index 3441f5e9a70..da7f707002a 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= diff --git a/libcontainer/cgroups/fs/memory.go b/libcontainer/cgroups/fs/memory.go index a0e78074980..783566d68f0 100644 --- a/libcontainer/cgroups/fs/memory.go +++ b/libcontainer/cgroups/fs/memory.go @@ -170,6 +170,10 @@ func (s *MemoryGroup) GetStats(path string, stats *cgroups.Stats) error { return err } stats.MemoryStats.SwapUsage = swapUsage + stats.MemoryStats.SwapOnlyUsage = cgroups.MemoryData{ + Usage: swapUsage.Usage - memoryUsage.Usage, + Failcnt: swapUsage.Failcnt - memoryUsage.Failcnt, + } kernelUsage, err := getMemoryData(path, "kmem") if err != nil { return err diff --git a/libcontainer/cgroups/fs/memory_test.go b/libcontainer/cgroups/fs/memory_test.go index d305a62a393..95e9d3cbaa4 100644 --- a/libcontainer/cgroups/fs/memory_test.go +++ b/libcontainer/cgroups/fs/memory_test.go @@ -249,12 +249,13 @@ func TestMemoryStats(t *testing.T) { t.Fatal(err) } expectedStats := cgroups.MemoryStats{ - Cache: 512, - Usage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, - SwapUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, - KernelUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, - Stats: map[string]uint64{"cache": 512, "rss": 1024}, - UseHierarchy: true, + Cache: 512, + Usage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, + SwapUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, + SwapOnlyUsage: cgroups.MemoryData{Usage: 0, MaxUsage: 0, Failcnt: 0, Limit: 0}, + KernelUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, + Stats: map[string]uint64{"cache": 512, "rss": 1024}, + UseHierarchy: true, PageUsageByNUMA: cgroups.PageUsageByNUMA{ PageUsageByNUMAInner: cgroups.PageUsageByNUMAInner{ Total: cgroups.PageStats{Total: 44611, Nodes: map[uint8]uint64{0: 32631, 1: 7501, 2: 1982, 3: 2497}}, diff --git a/libcontainer/cgroups/fs2/memory.go b/libcontainer/cgroups/fs2/memory.go index 9cca98c4c0a..01fe7d8e12d 100644 --- a/libcontainer/cgroups/fs2/memory.go +++ b/libcontainer/cgroups/fs2/memory.go @@ -100,7 +100,7 @@ func statMemory(dirPath string, stats *cgroups.Stats) error { memoryUsage, err := getMemoryDataV2(dirPath, "") if err != nil { if errors.Is(err, unix.ENOENT) && dirPath == UnifiedMountpoint { - // The root cgroup does not have memory.{current,max} + // The root cgroup does not have memory.{current,max,peak} // so emulate those using data from /proc/meminfo and // /sys/fs/cgroup/memory.stat return rootStatsFromMeminfo(stats) @@ -108,10 +108,12 @@ func statMemory(dirPath string, stats *cgroups.Stats) error { return err } stats.MemoryStats.Usage = memoryUsage - swapUsage, err := getMemoryDataV2(dirPath, "swap") + swapOnlyUsage, err := getMemoryDataV2(dirPath, "swap") if err != nil { return err } + stats.MemoryStats.SwapOnlyUsage = swapOnlyUsage + swapUsage := swapOnlyUsage // As cgroup v1 reports SwapUsage values as mem+swap combined, // while in cgroup v2 swap values do not include memory, // report combined mem+swap for v1 compatibility. @@ -119,6 +121,9 @@ func statMemory(dirPath string, stats *cgroups.Stats) error { if swapUsage.Limit != math.MaxUint64 { swapUsage.Limit += memoryUsage.Limit } + // The `MaxUsage` of mem+swap cannot simply combine mem with + // swap. So set it to 0 for v1 compatibility. + swapUsage.MaxUsage = 0 stats.MemoryStats.SwapUsage = swapUsage return nil @@ -133,6 +138,7 @@ func getMemoryDataV2(path, name string) (cgroups.MemoryData, error) { } usage := moduleName + ".current" limit := moduleName + ".max" + maxUsage := moduleName + ".peak" value, err := fscommon.GetCgroupParamUint(path, usage) if err != nil { @@ -152,6 +158,14 @@ func getMemoryDataV2(path, name string) (cgroups.MemoryData, error) { } memoryData.Limit = value + // `memory.peak` since kernel 5.19 + // `memory.swap.peak` since kernel 6.5 + value, err = fscommon.GetCgroupParamUint(path, maxUsage) + if err != nil && !os.IsNotExist(err) { + return cgroups.MemoryData{}, err + } + memoryData.MaxUsage = value + return memoryData, nil } diff --git a/libcontainer/cgroups/fs2/memory_test.go b/libcontainer/cgroups/fs2/memory_test.go index 2e2713c29ae..89c999d0cee 100644 --- a/libcontainer/cgroups/fs2/memory_test.go +++ b/libcontainer/cgroups/fs2/memory_test.go @@ -94,6 +94,10 @@ func TestStatMemoryPodCgroup(t *testing.T) { t.Fatal(err) } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.peak"), []byte("987654321"), 0o644); err != nil { + t.Fatal(err) + } + gotStats := cgroups.NewStats() // use a fake root path to trigger the pod cgroup lookup. @@ -107,6 +111,18 @@ func TestStatMemoryPodCgroup(t *testing.T) { if gotStats.MemoryStats.Usage.Usage != expectedUsageBytes { t.Errorf("parsed cgroupv2 memory.stat doesn't match expected result: \ngot %#v\nexpected %#v\n", gotStats.MemoryStats.Usage.Usage, expectedUsageBytes) } + + // result should be "memory.max" + var expectedLimitBytes uint64 = 999999999 + if gotStats.MemoryStats.Usage.Limit != expectedLimitBytes { + t.Errorf("parsed cgroupv2 memory.stat doesn't match expected result: \ngot %#v\nexpected %#v\n", gotStats.MemoryStats.Usage.Limit, expectedLimitBytes) + } + + // result should be "memory.peak" + var expectedMaxUsageBytes uint64 = 987654321 + if gotStats.MemoryStats.Usage.MaxUsage != expectedMaxUsageBytes { + t.Errorf("parsed cgroupv2 memory.stat doesn't match expected result: \ngot %#v\nexpected %#v\n", gotStats.MemoryStats.Usage.MaxUsage, expectedMaxUsageBytes) + } } func TestRootStatsFromMeminfo(t *testing.T) { diff --git a/libcontainer/cgroups/stats.go b/libcontainer/cgroups/stats.go index 40a81dd5a86..0d8371b05f1 100644 --- a/libcontainer/cgroups/stats.go +++ b/libcontainer/cgroups/stats.go @@ -78,6 +78,8 @@ type MemoryStats struct { Usage MemoryData `json:"usage,omitempty"` // usage of memory + swap SwapUsage MemoryData `json:"swap_usage,omitempty"` + // usage of swap only + SwapOnlyUsage MemoryData `json:"swap_only_usage,omitempty"` // usage of kernel memory KernelUsage MemoryData `json:"kernel_usage,omitempty"` // usage of kernel TCP memory diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index c1b4a0041c2..6ebf5ec7b60 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -21,9 +21,9 @@ type Rlimit struct { // IDMap represents UID/GID Mappings for User Namespaces. type IDMap struct { - ContainerID int `json:"container_id"` - HostID int `json:"host_id"` - Size int `json:"size"` + ContainerID int64 `json:"container_id"` + HostID int64 `json:"host_id"` + Size int64 `json:"size"` } // Seccomp represents syscall restrictions diff --git a/libcontainer/configs/config_linux.go b/libcontainer/configs/config_linux.go index 8c02848b70a..51fe940748a 100644 --- a/libcontainer/configs/config_linux.go +++ b/libcontainer/configs/config_linux.go @@ -1,6 +1,10 @@ package configs -import "errors" +import ( + "errors" + "fmt" + "math" +) var ( errNoUIDMap = errors.New("User namespaces enabled, but no uid mappings found.") @@ -16,11 +20,18 @@ func (c Config) HostUID(containerId int) (int, error) { if c.UidMappings == nil { return -1, errNoUIDMap } - id, found := c.hostIDFromMapping(containerId, c.UidMappings) + id, found := c.hostIDFromMapping(int64(containerId), c.UidMappings) if !found { return -1, errNoUserMap } - return id, nil + // If we are a 32-bit binary running on a 64-bit system, it's possible + // the mapped user is too large to store in an int, which means we + // cannot do the mapping. We can't just return an int64, because + // os.Setuid() takes an int. + if id > math.MaxInt { + return -1, fmt.Errorf("mapping for uid %d (host id %d) is larger than native integer size (%d)", containerId, id, math.MaxInt) + } + return int(id), nil } // Return unchanged id. return containerId, nil @@ -39,11 +50,18 @@ func (c Config) HostGID(containerId int) (int, error) { if c.GidMappings == nil { return -1, errNoGIDMap } - id, found := c.hostIDFromMapping(containerId, c.GidMappings) + id, found := c.hostIDFromMapping(int64(containerId), c.GidMappings) if !found { return -1, errNoGroupMap } - return id, nil + // If we are a 32-bit binary running on a 64-bit system, it's possible + // the mapped user is too large to store in an int, which means we + // cannot do the mapping. We can't just return an int64, because + // os.Setgid() takes an int. + if id > math.MaxInt { + return -1, fmt.Errorf("mapping for gid %d (host id %d) is larger than native integer size (%d)", containerId, id, math.MaxInt) + } + return int(id), nil } // Return unchanged id. return containerId, nil @@ -57,7 +75,7 @@ func (c Config) HostRootGID() (int, error) { // Utility function that gets a host ID for a container ID from user namespace map // if that ID is present in the map. -func (c Config) hostIDFromMapping(containerID int, uMap []IDMap) (int, bool) { +func (c Config) hostIDFromMapping(containerID int64, uMap []IDMap) (int64, bool) { for _, m := range uMap { if (containerID >= m.ContainerID) && (containerID <= (m.ContainerID + m.Size - 1)) { hostID := m.HostID + (containerID - m.ContainerID) diff --git a/libcontainer/configs/validate/rootless.go b/libcontainer/configs/validate/rootless.go index 9a6e5eb32a3..37c383366fe 100644 --- a/libcontainer/configs/validate/rootless.go +++ b/libcontainer/configs/validate/rootless.go @@ -28,25 +28,18 @@ func (v *ConfigValidator) rootlessEUID(config *configs.Config) error { return nil } -func hasIDMapping(id int, mappings []configs.IDMap) bool { - for _, m := range mappings { - if id >= m.ContainerID && id < m.ContainerID+m.Size { - return true - } - } - return false -} - func rootlessEUIDMappings(config *configs.Config) error { if !config.Namespaces.Contains(configs.NEWUSER) { return errors.New("rootless container requires user namespaces") } - - if len(config.UidMappings) == 0 { - return errors.New("rootless containers requires at least one UID mapping") - } - if len(config.GidMappings) == 0 { - return errors.New("rootless containers requires at least one GID mapping") + // We only require mappings if we are not joining another userns. + if path := config.Namespaces.PathOf(configs.NEWUSER); path == "" { + if len(config.UidMappings) == 0 { + return errors.New("rootless containers requires at least one UID mapping") + } + if len(config.GidMappings) == 0 { + return errors.New("rootless containers requires at least one GID mapping") + } } return nil } @@ -70,8 +63,8 @@ func rootlessEUIDMount(config *configs.Config) error { // Ignore unknown mount options. continue } - if !hasIDMapping(uid, config.UidMappings) { - return errors.New("cannot specify uid= mount options for unmapped uid in rootless containers") + if _, err := config.HostUID(uid); err != nil { + return fmt.Errorf("cannot specify uid=%d mount option for rootless container: %w", uid, err) } } @@ -82,8 +75,8 @@ func rootlessEUIDMount(config *configs.Config) error { // Ignore unknown mount options. continue } - if !hasIDMapping(gid, config.GidMappings) { - return errors.New("cannot specify gid= mount options for unmapped gid in rootless containers") + if _, err := config.HostGID(gid); err != nil { + return fmt.Errorf("cannot specify gid=%d mount option for rootless container: %w", gid, err) } } } diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go index 4fbd308dadd..ece70a45d31 100644 --- a/libcontainer/configs/validate/validator.go +++ b/libcontainer/configs/validate/validator.go @@ -109,11 +109,19 @@ func (v *ConfigValidator) security(config *configs.Config) error { func (v *ConfigValidator) usernamespace(config *configs.Config) error { if config.Namespaces.Contains(configs.NEWUSER) { if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { - return errors.New("USER namespaces aren't enabled in the kernel") + return errors.New("user namespaces aren't enabled in the kernel") } + hasPath := config.Namespaces.PathOf(configs.NEWUSER) != "" + hasMappings := config.UidMappings != nil || config.GidMappings != nil + if !hasPath && !hasMappings { + return errors.New("user namespaces enabled, but no namespace path to join nor mappings to apply specified") + } + // The hasPath && hasMappings validation case is handled in specconv -- + // we cache the mappings in Config during specconv in the hasPath case, + // so we cannot do that validation here. } else { if config.UidMappings != nil || config.GidMappings != nil { - return errors.New("User namespace mappings specified, but USER namespace isn't enabled in the config") + return errors.New("user namespace mappings specified, but user namespace isn't enabled in the config") } } return nil diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go index 5181333fb12..e89a60b08c5 100644 --- a/libcontainer/configs/validate/validator_test.go +++ b/libcontainer/configs/validate/validator_test.go @@ -150,7 +150,7 @@ func TestValidateSecurityWithoutNEWNS(t *testing.T) { } } -func TestValidateUsernamespace(t *testing.T) { +func TestValidateUserNamespace(t *testing.T) { if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { t.Skip("Test requires userns.") } @@ -161,6 +161,8 @@ func TestValidateUsernamespace(t *testing.T) { {Type: configs.NEWUSER}, }, ), + UidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}}, + GidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}}, } validator := New() @@ -170,11 +172,11 @@ func TestValidateUsernamespace(t *testing.T) { } } -func TestValidateUsernamespaceWithoutUserNS(t *testing.T) { - uidMap := configs.IDMap{ContainerID: 123} +func TestValidateUsernsMappingWithoutNamespace(t *testing.T) { config := &configs.Config{ Rootfs: "/var", - UidMappings: []configs.IDMap{uidMap}, + UidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}}, + GidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}}, } validator := New() diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index 9cc5ebb33cf..40b332f9810 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -2277,7 +2277,7 @@ func ignoreTerminateErrors(err error) error { func requiresRootOrMappingTool(c *configs.Config) bool { gidMap := []configs.IDMap{ - {ContainerID: 0, HostID: os.Getegid(), Size: 1}, + {ContainerID: 0, HostID: int64(os.Getegid()), Size: 1}, } return !reflect.DeepEqual(c.GidMappings, gidMap) } diff --git a/libcontainer/integration/exec_test.go b/libcontainer/integration/exec_test.go index 5c6272e5d42..670b33fcbb3 100644 --- a/libcontainer/integration/exec_test.go +++ b/libcontainer/integration/exec_test.go @@ -18,6 +18,7 @@ import ( "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/cgroups/systemd" "github.com/opencontainers/runc/libcontainer/configs" + "github.com/opencontainers/runc/libcontainer/userns" "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/sys/unix" @@ -1588,6 +1589,11 @@ func TestInitJoinNetworkAndUser(t *testing.T) { config2 := newTemplateConfig(t, &tParam{userns: true}) config2.Namespaces.Add(configs.NEWNET, netns1) config2.Namespaces.Add(configs.NEWUSER, userns1) + // Emulate specconv.setupUserNamespace(). + uidMap, gidMap, err := userns.GetUserNamespaceMappings(userns1) + ok(t, err) + config2.UidMappings = uidMap + config2.GidMappings = gidMap config2.Cgroups.Path = "integration/test2" container2, err := newContainer(t, config2) ok(t, err) diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 1b358b24b4d..7dbfb8691d0 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -18,6 +18,7 @@ import ( "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/devices" "github.com/opencontainers/runc/libcontainer/seccomp" + "github.com/opencontainers/runc/libcontainer/userns" libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" @@ -926,9 +927,9 @@ next: func setupUserNamespace(spec *specs.Spec, config *configs.Config) error { create := func(m specs.LinuxIDMapping) configs.IDMap { return configs.IDMap{ - HostID: int(m.HostID), - ContainerID: int(m.ContainerID), - Size: int(m.Size), + HostID: int64(m.HostID), + ContainerID: int64(m.ContainerID), + Size: int64(m.Size), } } if spec.Linux != nil { @@ -939,6 +940,40 @@ func setupUserNamespace(spec *specs.Spec, config *configs.Config) error { config.GidMappings = append(config.GidMappings, create(m)) } } + if path := config.Namespaces.PathOf(configs.NEWUSER); path != "" { + // Cache the current userns mappings in our configuration, so that we + // can calculate uid and gid mappings within runc. These mappings are + // never used for configuring the container if the path is set. + uidMap, gidMap, err := userns.GetUserNamespaceMappings(path) + if err != nil { + return fmt.Errorf("failed to cache mappings for userns: %w", err) + } + // We cannot allow uid or gid mappings to be set if we are also asked + // to join a userns. + if config.UidMappings != nil || config.GidMappings != nil { + // FIXME: It turns out that containerd and CRIO pass both a userns + // path and the mappings of the namespace in the same config.json. + // Such a configuration is technically not valid, but we used to + // require mappings be specified, and thus users worked around our + // bug -- so we can't regress it at the moment. But we also don't + // want to produce broken behaviour if the mapping doesn't match + // the userns. So (for now) we output a warning if the actual + // userns mappings match the configuration, otherwise we return an + // error. + if !userns.IsSameMapping(uidMap, config.UidMappings) || + !userns.IsSameMapping(gidMap, config.GidMappings) { + return errors.New("user namespaces enabled, but both namespace path and non-matching mapping specified -- you may only provide one") + } + logrus.Warnf("config.json has both a userns path to join and a matching userns mapping specified -- you may only provide one. Future versions of runc may return an error with this configuration, please report a bug on if you see this warning and cannot update your configuration.") + } + + config.UidMappings = uidMap + config.GidMappings = gidMap + logrus.WithFields(logrus.Fields{ + "uid_map": uidMap, + "gid_map": gidMap, + }).Debugf("config uses path-based userns configuration -- current uid and gid mappings cached") + } rootUID, err := config.HostRootUID() if err != nil { return err diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go index 79768e31861..f79ea0e3727 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -610,6 +610,40 @@ func TestDupNamespaces(t *testing.T) { } } +func TestUserNamespaceMappingAndPath(t *testing.T) { + if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { + t.Skip("Test requires userns.") + } + + spec := &specs.Spec{ + Root: &specs.Root{ + Path: "rootfs", + }, + Linux: &specs.Linux{ + UIDMappings: []specs.LinuxIDMapping{ + {ContainerID: 0, HostID: 1000, Size: 1000}, + }, + GIDMappings: []specs.LinuxIDMapping{ + {ContainerID: 0, HostID: 2000, Size: 1000}, + }, + Namespaces: []specs.LinuxNamespace{ + { + Type: "user", + Path: "/proc/1/ns/user", + }, + }, + }, + } + + _, err := CreateLibcontainerConfig(&CreateOpts{ + Spec: spec, + }) + + if !strings.Contains(err.Error(), "both namespace path and non-matching mapping specified") { + t.Errorf("user namespace with path and non-matching mapping should be forbidden, got error %v", err) + } +} + func TestNonZeroEUIDCompatibleSpecconvValidate(t *testing.T) { if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { t.Skip("Test requires userns.") diff --git a/libcontainer/userns/userns_maps.c b/libcontainer/userns/userns_maps.c new file mode 100644 index 00000000000..84f2c6188c3 --- /dev/null +++ b/libcontainer/userns/userns_maps.c @@ -0,0 +1,79 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +/* + * All of the code here is run inside an aync-signal-safe context, so we need + * to be careful to not call any functions that could cause issues. In theory, + * since we are a Go program, there are fewer restrictions in practice, it's + * better to be safe than sorry. + * + * The only exception is exit, which we need to call to make sure we don't + * return into runc. + */ + +void bail(int pipefd, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + vdprintf(pipefd, fmt, args); + va_end(args); + + exit(1); +} + +int spawn_userns_cat(char *userns_path, char *path, int outfd, int errfd) +{ + char buffer[4096] = { 0 }; + + pid_t child = fork(); + if (child != 0) + return child; + /* in child */ + + /* Join the target userns. */ + int nsfd = open(userns_path, O_RDONLY); + if (nsfd < 0) + bail(errfd, "open userns path %s failed: %m", userns_path); + + int err = setns(nsfd, CLONE_NEWUSER); + if (err < 0) + bail(errfd, "setns %s failed: %m", userns_path); + + close(nsfd); + + /* Pipe the requested file contents. */ + int fd = open(path, O_RDONLY); + if (fd < 0) + bail(errfd, "open %s in userns %s failed: %m", path, userns_path); + + int nread, ntotal = 0; + while ((nread = read(fd, buffer, sizeof(buffer))) != 0) { + if (nread < 0) + bail(errfd, "read bytes from %s failed (after %d total bytes read): %m", path, ntotal); + ntotal += nread; + + int nwritten = 0; + while (nwritten < nread) { + int n = write(outfd, buffer, nread - nwritten); + if (n < 0) + bail(errfd, "write %d bytes from %s failed (after %d bytes written): %m", + nread - nwritten, path, nwritten); + nwritten += n; + } + if (nread != nwritten) + bail(errfd, "mismatch for bytes read and written: %d read != %d written", nread, nwritten); + } + + close(fd); + close(outfd); + close(errfd); + + /* We must exit here, otherwise we would return into a forked runc. */ + exit(0); +} diff --git a/libcontainer/userns/userns_maps_linux.go b/libcontainer/userns/userns_maps_linux.go new file mode 100644 index 00000000000..7a8c2b023b3 --- /dev/null +++ b/libcontainer/userns/userns_maps_linux.go @@ -0,0 +1,186 @@ +//go:build linux + +package userns + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "unsafe" + + "github.com/opencontainers/runc/libcontainer/configs" + "github.com/sirupsen/logrus" +) + +/* +#include +extern int spawn_userns_cat(char *userns_path, char *path, int outfd, int errfd); +*/ +import "C" + +func parseIdmapData(data []byte) (ms []configs.IDMap, err error) { + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + var m configs.IDMap + line := scanner.Text() + if _, err := fmt.Sscanf(line, "%d %d %d", &m.ContainerID, &m.HostID, &m.Size); err != nil { + return nil, fmt.Errorf("parsing id map failed: invalid format in line %q: %w", line, err) + } + ms = append(ms, m) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("parsing id map failed: %w", err) + } + return ms, nil +} + +// Do something equivalent to nsenter --user= cat , but more +// efficiently. Returns the contents of the requested file from within the user +// namespace. +func spawnUserNamespaceCat(nsPath string, path string) ([]byte, error) { + rdr, wtr, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("create pipe for userns spawn failed: %w", err) + } + defer rdr.Close() + defer wtr.Close() + + errRdr, errWtr, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("create error pipe for userns spawn failed: %w", err) + } + defer errRdr.Close() + defer errWtr.Close() + + cNsPath := C.CString(nsPath) + defer C.free(unsafe.Pointer(cNsPath)) + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) + + childPid := C.spawn_userns_cat(cNsPath, cPath, C.int(wtr.Fd()), C.int(errWtr.Fd())) + + if childPid < 0 { + return nil, fmt.Errorf("failed to spawn fork for userns") + } else if childPid == 0 { + // this should never happen + panic("runc executing inside fork child -- unsafe state!") + } + + // We are in the parent -- close the write end of the pipe before reading. + wtr.Close() + output, err := io.ReadAll(rdr) + rdr.Close() + if err != nil { + return nil, fmt.Errorf("reading from userns spawn failed: %w", err) + } + + // Ditto for the error pipe. + errWtr.Close() + errOutput, err := io.ReadAll(errRdr) + errRdr.Close() + if err != nil { + return nil, fmt.Errorf("reading from userns spawn error pipe failed: %w", err) + } + errOutput = bytes.TrimSpace(errOutput) + + // Clean up the child. + child, err := os.FindProcess(int(childPid)) + if err != nil { + return nil, fmt.Errorf("could not find userns spawn process: %w", err) + } + state, err := child.Wait() + if err != nil { + return nil, fmt.Errorf("failed to wait for userns spawn process: %w", err) + } + if !state.Success() { + errStr := string(errOutput) + if errStr == "" { + errStr = fmt.Sprintf("unknown error (status code %d)", state.ExitCode()) + } + return nil, fmt.Errorf("userns spawn: %s", errStr) + } else if len(errOutput) > 0 { + // We can just ignore weird output in the error pipe if the process + // didn't bail(), but for completeness output for debugging. + logrus.Debugf("userns spawn succeeded but unexpected error message found: %s", string(errOutput)) + } + // The subprocess succeeded, return whatever it wrote to the pipe. + return output, nil +} + +func GetUserNamespaceMappings(nsPath string) (uidMap, gidMap []configs.IDMap, err error) { + var ( + pid int + extra rune + tryFastPath bool + ) + + // nsPath is usually of the form /proc//ns/user, which means that we + // already have a pid that is part of the user namespace and thus we can + // just use the pid to read from /proc//*id_map. + // + // Note that Sscanf doesn't consume the whole input, so we check for any + // trailing data with %c. That way, we can be sure the pattern matched + // /proc/$pid/ns/user _exactly_ iff n === 1. + if n, _ := fmt.Sscanf(nsPath, "/proc/%d/ns/user%c", &pid, &extra); n == 1 { + tryFastPath = pid > 0 + } + + for _, mapType := range []struct { + name string + idMap *[]configs.IDMap + }{ + {"uid_map", &uidMap}, + {"gid_map", &gidMap}, + } { + var mapData []byte + + if tryFastPath { + path := fmt.Sprintf("/proc/%d/%s", pid, mapType.name) + data, err := os.ReadFile(path) + if err != nil { + // Do not error out here -- we need to try the slow path if the + // fast path failed. + logrus.Debugf("failed to use fast path to read %s from userns %s (error: %s), falling back to slow userns-join path", mapType.name, nsPath, err) + } else { + mapData = data + } + } else { + logrus.Debugf("cannot use fast path to read %s from userns %s, falling back to slow userns-join path", mapType.name, nsPath) + } + + if mapData == nil { + // We have to actually join the namespace if we cannot take the + // fast path. The path is resolved with respect to the child + // process, so just use /proc/self. + data, err := spawnUserNamespaceCat(nsPath, "/proc/self/"+mapType.name) + if err != nil { + return nil, nil, err + } + mapData = data + } + idMap, err := parseIdmapData(mapData) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse %s of userns %s: %w", mapType.name, nsPath, err) + } + *mapType.idMap = idMap + } + + return uidMap, gidMap, nil +} + +// IsSameMapping returns whether or not the two id mappings are the same. Note +// that if the order of the mappings is different, or a mapping has been split, +// the mappings will be considered different. +func IsSameMapping(a, b []configs.IDMap) bool { + if len(a) != len(b) { + return false + } + for idx := range a { + if a[idx] != b[idx] { + return false + } + } + return true +} diff --git a/runc.keyring b/runc.keyring index 84b4ca38a1e..3fb6d283636 100644 --- a/runc.keyring +++ b/runc.keyring @@ -72,18 +72,18 @@ KWr9ByCKAwVHsaSgVSJE/dse4f1toqeEHHbWk682U4RqOWZR4bA0 pub ed25519 2019-06-21 [C] C9C370B246B09F6DBCFC744C34401015D1D2D386 uid [ultimate] Aleksa Sarai -sub ed25519 2022-09-30 [S] [expires: 2024-09-29] -sub cv25519 2022-09-30 [E] [expires: 2024-09-29] -sub ed25519 2022-09-30 [A] [expires: 2024-09-29] +sub ed25519 2022-09-30 [S] [expires: 2030-03-25] +sub cv25519 2022-09-30 [E] [expires: 2030-03-25] +sub ed25519 2022-09-30 [A] [expires: 2030-03-25] -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: github=cyphar mDMEXQxvLxYJKwYBBAHaRw8BAQdArRQoZs9YzYtQIiPA1qdvUT8Q0wbPZyRV65Tz QNTIZla0IEFsZWtzYSBTYXJhaSA8Y3lwaGFyQGN5cGhhci5jb20+iJAEExYIADgF -CwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTJw3CyRrCfbbz8dEw0QBAV0dLThgUC -XQzCHwIbAQAKCRA0QBAV0dLThvUpAP9SwyOijLqEBz1A9pTqRAB0l/r+ABq+iUmH -UjMHO34LZAD/biRuAadaxIYJtmn7nKA55doyN2fQXhjArqypJ1SQywi4MwRdDMJS +CwkIBwIGFQoJCAsCBBYCAwECHgECF4ACGwEWIQTJw3CyRrCfbbz8dEw0QBAV0dLT +hgUCZa3xwQAKCRA0QBAV0dLThpQyAQDGzjZyyWWmd6Ykg5/lymp2MLIg1f2jG6ew +AiPT4ATkBAD/RgdLDf1IQStEH7pHmQa1qvqyRq1jeEgF23KruXbbdQ64MwRdDMJS FgkrBgEEAdpHDwEBB0B2IGusH7LuDH3hNT6JYM30S7G92FGogA6a9WQzKRlqvIh4 BCgWCgAgFiEEycNwskawn228/HRMNEAQFdHS04YFAmM2ukUCHQEACgkQNEAQFdHS 04ZTQAEAjAT0fXVJHdRL6UMCxDYsgjG+QyH1mr7gKgbPvB8A5LgBAN4QDqCxIY3b @@ -106,20 +106,20 @@ o7lcWozXFlQDOM7eoT4avvWOVcsaD4h4BBgWCAAgFiEEycNwskawn228/HRMNEAQ FdHS04YFAl0Mwo0CGyAACgkQNEAQFdHS04ajxQEAsZf1yDORUVYicREc/7z0U+51 DJzeAexeJTYM+N+x13EA/0Ex+o7qQ7dZLGDn7x4LSbd39C+++suHsEaE4XwlX6cH uDMEYza6SxYJKwYBBAHaRw8BAQdAE3s7dZQFuImQX2tWshIdGjeUKZc7rlMcrZ6+ -q25gaH2I9QQYFgoAJhYhBMnDcLJGsJ9tvPx0TDRAEBXR0tOGBQJjNrpLAhsCBQkD -wmcAAIEJEDRAEBXR0tOGdiAEGRYKAB0WIQS2TklVsp+j1GPyqQYol/rSt+lEbwUC -Yza6SwAKCRAol/rSt+lEb9obAQC8ij4yJTU7ZcAtTx2ZMjj8EoruGb3ku6VpRyx1 -+pyQQgD/QgQ7X1G7xtwuVpY0kHYga1yoKLA2ycT8F8PrVtF7pAMWkgD9EWe1E77C -BVd//i3ib+h9ikCeJ+gaxc6aU24ZBcN2tfUBAJmCmYQ0VEbXyvCqkdJEQ4qk5Y9C -2V4w83dj4a5RYKUGuDgEYza6YBIKKwYBBAGXVQEFAQEHQKECW5Y7nUGCka0/WcCM -OerRY95Pm2DQVL76QzvhXD8tAwEIB4h+BBgWCgAmFiEEycNwskawn228/HRMNEAQ -FdHS04YFAmM2umACGwwFCQPCZwAACgkQNEAQFdHS04bkuwEA7AEL+iSPlA8/YILp -0sFMzmtRqTDMqx2BY8K5wEk9fusA/jAhbeJw57bZYvK4MghfUa9tRocyII84UmOA -cgDbPPIFuDMEYza6bhYJKwYBBAHaRw8BAQdAgHXd0yf6MPXJZCZ3TFz8xLymyPsD -TF2SQwwqM4+nYbeIfgQYFgoAJhYhBMnDcLJGsJ9tvPx0TDRAEBXR0tOGBQJjNrpu -AhsgBQkDwmcAAAoJEDRAEBXR0tOGB8UA/0wf8uECKMmXGQ4DNi+ei2E9Ft6GL8qw -UGjwM/EKH2RoAP9HNRRKBjDxs/AZ3pBx1Q8hnHELLo0kXPc+3BG6Pht5BA== -=KN4V +q25gaH2I9QQYFgoAJgIbAhYhBMnDcLJGsJ9tvPx0TDRAEBXR0tOGBQJlrfJcBQkO +EpjFAIF2IAQZFgoAHRYhBLZOSVWyn6PUY/KpBiiX+tK36URvBQJjNrpLAAoJECiX ++tK36URv2hsBALyKPjIlNTtlwC1PHZkyOPwSiu4ZveS7pWlHLHX6nJBCAP9CBDtf +UbvG3C5WljSQdiBrXKgosDbJxPwXw+tW0XukAwkQNEAQFdHS04bMkQEA9elVwA0A ++ywDw+jnifIc98XqLI+KF3Xl0A9+lMuwthMBAO00DeAEjkryFMGp62GPNHqr/r6p ++6DIeUjWgK4Sh8IMuDgEYza6YBIKKwYBBAGXVQEFAQEHQKECW5Y7nUGCka0/WcCM +OerRY95Pm2DQVL76QzvhXD8tAwEIB4h+BBgWCgAmAhsMFiEEycNwskawn228/HRM +NEAQFdHS04YFAmWt8lwFCQ4SmLAACgkQNEAQFdHS04apHgD+MIRj2kujpxtQt04D +ZB+hofBtHIEMo2tplFBYvhZ6KOMA/1q3aRv6jnWAv8woc50KitP4/+iPmfyzaBA/ +8XA5DdIKuDMEYza6bhYJKwYBBAHaRw8BAQdAgHXd0yf6MPXJZCZ3TFz8xLymyPsD +TF2SQwwqM4+nYbeIfgQYFgoAJgIbIBYhBMnDcLJGsJ9tvPx0TDRAEBXR0tOGBQJl +rfJcBQkOEpiiAAoJEDRAEBXR0tOGAUwA/jbaz04OXnV3PYC/yQUsUJsihCTqz4Ne +lxxclgJYU604APsFzpoLD0oUlfMn5Fh75ftkKPrwiHpTj4rRU6oIQu1/Bg== +=Ab7w -----END PGP PUBLIC KEY BLOCK----- pub rsa2048 2020-04-28 [SC] [expires: 2025-04-18] @@ -159,11 +159,11 @@ S1NPMeS7+G/gPN9Ze9qFmOF2p57cmEa+8mriZCYY3BcUBOiMOV5HSBKJwqA2M8au =GkpD -----END PGP PUBLIC KEY BLOCK----- -pub rsa3072 2019-07-25 [SC] [expires: 2023-11-02] +pub rsa3072 2019-07-25 [SC] [expires: 2025-07-27] C020EA876CE4E06C7AB95AEF49524C6F9F638F1A uid [ultimate] Akihiro Suda uid [ultimate] Akihiro Suda -sub rsa3072 2019-07-25 [E] [expires: 2023-11-02] +sub rsa3072 2019-07-25 [E] [expires: 2025-07-27] -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: github=AkihiroSuda @@ -178,26 +178,26 @@ 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 @@ -206,16 +206,16 @@ Qf6sg0b/k6/vkVveopeeH28zb/nnVuhgGSxcbiZUrFC9EfhX4/6NNFRhE300AjeF bP7SoXx3qRhr993BDSP32r44hy+kYLhZP5K5oXivcITJZuGcJh49P4QuYGrnODIL gEhedWeePcJXFcEz09teizlWKGzd+EA3uwYd/bQelflwXkGuCLaoNv4qcH3oJDp1 vYI0zT7hGvnz3thRLg3SOWFq5cBhnfNGXPLsoNZBzWGn2cm5MJYSKjIM470AEQEA -AYkBvAQYAQoAJgIbDBYhBMAg6ods5OBserla70lSTG+fY48aBQJhgNRTBQkICSI0 -AAoJEElSTG+fY48a3YML/3snhGBx/Xd0EcK0pzyvyivZwavlGsQPAF2c1Rj7Lr1i -eUrp6CZ/yW7/oAvlk6Ngc0SoWba/pgnz7bVQEc21JTY86M1bRLLh3fmYCx8YFbsR -43zVr2bxDledzKV3bIuWStWbljHECuNTT91907pc3r4jv+jN4ZaXVUQ9pXj0DrV+ -MTJVCo7nrEXiq6q1WqaUAV9dMQE3rWGFa2u45QCZGLckOu3cuSCU8CVxSScmxgII -bUBu17xDzQnDkdcEQzzkZtDOrwF76dPdlrW69PXtC9oElRJbGCERivqlrpKDagXI -h4eZYfcFb2gc0qZjblvfVHiot65WM9bUsSAUAEfskYqIGLshzV9MrxFYQYvgt3ym -Qs7D8ORJiphjaOvDeqVyGdPm/rN5SVMVGYpJX6EkZkHinV/kRChtuLAD7NQ3YH5O -5l+Ehze9Nm4laEXQC/tme9B1XH0PUBJk1x8NeoVrYCTnypVFfRw37mC9XBu5TF6U -ix7vx45U/EvZrqmkDrEFOQ== -=4+1P +AYkBvAQYAQoAJgIbDBYhBMAg6ods5OBserla70lSTG+fY48aBQJkw8uyBQkLTBmT +AAoJEElSTG+fY48ayhsL+gLvKlfkYgxodyWKR5hOiUMKWE5tqfQY6kqrgssPYw+u +Fn69AamQLt4I2AHRg0AHjoZEsMfR19uXZ24XwwcWwgWU6yRJgMSIK67bLvL+d686 +m2KQ2PpmfDrizUgY4J0sY+tzwNZeWxQiFy/Ni6AdEqJvJQDsrKYJ2GGWm6JMZCPw +y3h5ouueieiEc0pvwEz2kg64uv6p8SUV1me66IXQaGseXb/BcW+Ap2WJO+IZjtNB +qhk+V+1x5ZT6s9RecjiTDmKfZ71zyRWplkfL22+4XVEc3qLS3r0ZSzeIA4JPRf+N +yCGjavdTNgu2bTo8iSgBq2NRT9kNwTaS8j883L0eY/JJktrfWnWE4qAuXBqLzkIl +smspRWy0byLQrrzk9stncF/CDt5XuHPcsXOcRVXVyM+/RXqWKdNAwZO67HD4wJR9 +YR4avhGZZXguH3b0ka2zO8sxTju/09yb07NJ2qfjfWSHCmaj9KuhhE0EO625tckS +58ceqolNBtrydoYZOc2CKw== +=ol6W -----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/integration/run.bats b/tests/integration/run.bats index e59ccb4a43d..88540c763d1 100644 --- a/tests/integration/run.bats +++ b/tests/integration/run.bats @@ -108,3 +108,60 @@ function teardown() { [ "$status" -eq 0 ] [ "$output" = "410" ] } + +@test "runc run [joining existing container namespaces]" { + # Create a detached container with the namespaces we want. We notably want + # to include userns, which requires config-related configuration. + if [ $EUID -eq 0 ]; then + update_config '.linux.namespaces += [{"type": "user"}] + | .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 100}] + | .linux.gidMappings += [{"containerID": 0, "hostID": 200000, "size": 200}]' + mkdir -p rootfs/{proc,sys,tmp} + fi + update_config '.process.args = ["sleep", "infinity"]' + + runc run -d --console-socket "$CONSOLE_SOCKET" target_ctr + [ "$status" -eq 0 ] + + # Modify our container's configuration such that it is just going to + # inherit all of the namespaces of the target container. + # + # NOTE: We cannot join the mount namespace of another container because of + # some quirks of the runtime-spec. In particular, we MUST pivot_root into + # root.path and root.path MUST be set in the config, so runc cannot just + # ignore root.path when joining namespaces (and root.path doesn't exist + # inside root.path, for obvious reasons). + # + # We could hack around this (create a copy of the rootfs inside the rootfs, + # or use a simpler mount namespace target), but those wouldn't be similar + # tests to the other namespace joining tests. + target_pid="$(__runc state target_ctr | jq .pid)" + update_config '.linux.namespaces |= map_values(.path = if .type == "mount" then "" else "/proc/'"$target_pid"'/ns/" + ({"network": "net", "mount": "mnt"}[.type] // .type) end)' + # Remove the userns configuration (it cannot be changed). + update_config '.linux |= (del(.uidMappings) | del(.gidMappings))' + + runc run -d --console-socket "$CONSOLE_SOCKET" attached_ctr + [ "$status" -eq 0 ] + + # Make sure there are two sleep processes in our container. + runc exec attached_ctr ps aux + [ "$status" -eq 0 ] + run -0 grep "sleep infinity" <<<"$output" + [ "${#lines[@]}" -eq 2 ] + + # ... that the userns mappings are the same... + runc exec attached_ctr cat /proc/self/uid_map + [ "$status" -eq 0 ] + if [ $EUID -eq 0 ]; then + grep -E '^\s+0\s+100000\s+100$' <<<"$output" + else + grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output" + fi + runc exec attached_ctr cat /proc/self/gid_map + [ "$status" -eq 0 ] + if [ $EUID -eq 0 ]; then + grep -E '^\s+0\s+200000\s+200$' <<<"$output" + else + grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output" + fi +} diff --git a/tests/integration/userns.bats b/tests/integration/userns.bats index eca1e0ce58a..2329119a2fe 100644 --- a/tests/integration/userns.bats +++ b/tests/integration/userns.bats @@ -15,15 +15,25 @@ function setup() { mkdir -p rootfs/{proc,sys,tmp} mkdir -p rootfs/tmp/mount-{1,2} + to_umount_list="$(mktemp "$BATS_RUN_TMPDIR/userns-mounts.XXXXXX")" if [ "$ROOTLESS" -eq 0 ]; then update_config ' .linux.namespaces += [{"type": "user"}] | .linux.uidMappings += [{"hostID": 100000, "containerID": 0, "size": 65534}] - | .linux.gidMappings += [{"hostID": 100000, "containerID": 0, "size": 65534}] ' + | .linux.gidMappings += [{"hostID": 200000, "containerID": 0, "size": 65534}] ' fi } function teardown() { teardown_bundle + + if [ -v to_umount_list ]; then + while read -r mount_path; do + umount -l "$mount_path" || : + rm -f "$mount_path" + done <"$to_umount_list" + rm -f "$to_umount_list" + unset to_umount_list + fi } @test "userns with simple mount" { @@ -83,3 +93,74 @@ function teardown() { runc exec test_busybox cat /sys/fs/cgroup/{pids,memory}/tasks [ "$status" -eq 0 ] } + +@test "userns join other container userns" { + # Create a detached container with the id-mapping we want. + update_config '.process.args = ["sleep", "infinity"]' + runc run -d --console-socket "$CONSOLE_SOCKET" target_userns + [ "$status" -eq 0 ] + + # Configure our container to attach to the first container's userns. + target_pid="$(__runc state target_userns | jq .pid)" + update_config '.linux.namespaces |= map(if .type == "user" then (.path = "/proc/'"$target_pid"'/ns/" + .type) else . end) + | del(.linux.uidMappings) + | del(.linux.gidMappings)' + runc run -d --console-socket "$CONSOLE_SOCKET" in_userns + [ "$status" -eq 0 ] + + runc exec in_userns cat /proc/self/uid_map + [ "$status" -eq 0 ] + if [ $EUID -eq 0 ]; then + grep -E '^\s+0\s+100000\s+65534$' <<<"$output" + else + grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output" + fi + + runc exec in_userns cat /proc/self/gid_map + [ "$status" -eq 0 ] + if [ $EUID -eq 0 ]; then + grep -E '^\s+0\s+200000\s+65534$' <<<"$output" + else + grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output" + fi +} + +@test "userns join other container userns [bind-mounted nsfd]" { + requires root + + # Create a detached container with the id-mapping we want. + update_config '.process.args = ["sleep", "infinity"]' + runc run -d --console-socket "$CONSOLE_SOCKET" target_userns + [ "$status" -eq 0 ] + + # Bind-mount the first containers userns nsfd to a different path, to + # exercise the non-fast-path (where runc has to join the userns to get the + # mappings). + target_pid="$(__runc state target_userns | jq .pid)" + userns_path=$(mktemp "$BATS_RUN_TMPDIR/userns.XXXXXX") + mount --bind "/proc/$target_pid/ns/user" "$userns_path" + echo "$userns_path" >>"$to_umount_list" + + # Configure our container to attach to the first container's userns. + update_config '.linux.namespaces |= map(if .type == "user" then (.path = "'"$userns_path"'") else . end) + | del(.linux.uidMappings) + | del(.linux.gidMappings)' + runc run -d --console-socket "$CONSOLE_SOCKET" in_userns + [ "$status" -eq 0 ] + + runc exec in_userns cat /proc/self/uid_map + [ "$status" -eq 0 ] + if [ $EUID -eq 0 ]; then + grep -E '^\s+0\s+100000\s+65534$' <<<"$output" + else + grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output" + fi + + runc exec in_userns cat /proc/self/gid_map + [ "$status" -eq 0 ] + if [ $EUID -eq 0 ]; then + grep -E '^\s+0\s+200000\s+65534$' <<<"$output" + else + grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output" + fi +} diff --git a/vendor/github.com/cyphar/filepath-securejoin/.travis.yml b/vendor/github.com/cyphar/filepath-securejoin/.travis.yml deleted file mode 100644 index b94ff8cf92a..00000000000 --- a/vendor/github.com/cyphar/filepath-securejoin/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2017 SUSE LLC. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -language: go -go: - - 1.13.x - - 1.16.x - - tip -arch: - - AMD64 - - ppc64le -os: - - linux - - osx - -script: - - go test -cover -v ./... - -notifications: - email: false diff --git a/vendor/github.com/cyphar/filepath-securejoin/README.md b/vendor/github.com/cyphar/filepath-securejoin/README.md index 3624617c89b..4eca0f23550 100644 --- a/vendor/github.com/cyphar/filepath-securejoin/README.md +++ b/vendor/github.com/cyphar/filepath-securejoin/README.md @@ -1,6 +1,6 @@ ## `filepath-securejoin` ## -[![Build Status](https://travis-ci.org/cyphar/filepath-securejoin.svg?branch=master)](https://travis-ci.org/cyphar/filepath-securejoin) +[![Build Status](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml/badge.svg)](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml) An implementation of `SecureJoin`, a [candidate for inclusion in the Go standard library][go#20126]. The purpose of this function is to be a "secure" diff --git a/vendor/github.com/cyphar/filepath-securejoin/VERSION b/vendor/github.com/cyphar/filepath-securejoin/VERSION index 7179039691c..abd410582de 100644 --- a/vendor/github.com/cyphar/filepath-securejoin/VERSION +++ b/vendor/github.com/cyphar/filepath-securejoin/VERSION @@ -1 +1 @@ -0.2.3 +0.2.4 diff --git a/vendor/github.com/cyphar/filepath-securejoin/join.go b/vendor/github.com/cyphar/filepath-securejoin/join.go index 7dd08dbbdf7..aa32b85fb84 100644 --- a/vendor/github.com/cyphar/filepath-securejoin/join.go +++ b/vendor/github.com/cyphar/filepath-securejoin/join.go @@ -39,17 +39,27 @@ func IsNotExist(err error) bool { // components in the returned string are not modified (in other words are not // replaced with symlinks on the filesystem) after this function has returned. // Such a symlink race is necessarily out-of-scope of SecureJoin. +// +// Volume names in unsafePath are always discarded, regardless if they are +// provided via direct input or when evaluating symlinks. Therefore: +// +// "C:\Temp" + "D:\path\to\file.txt" results in "C:\Temp\path\to\file.txt" func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { // Use the os.* VFS implementation if none was specified. if vfs == nil { vfs = osVFS{} } + unsafePath = filepath.FromSlash(unsafePath) var path bytes.Buffer n := 0 for unsafePath != "" { if n > 255 { - return "", &os.PathError{Op: "SecureJoin", Path: root + "/" + unsafePath, Err: syscall.ELOOP} + return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP} + } + + if v := filepath.VolumeName(unsafePath); v != "" { + unsafePath = unsafePath[len(v):] } // Next path component, p. diff --git a/vendor/modules.txt b/vendor/modules.txt index 6824560a91b..498b44c1e97 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -20,7 +20,7 @@ github.com/coreos/go-systemd/v22/dbus # github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d ## explicit; go 1.12 github.com/cpuguy83/go-md2man/v2/md2man -# github.com/cyphar/filepath-securejoin v0.2.3 +# github.com/cyphar/filepath-securejoin v0.2.4 ## explicit; go 1.13 github.com/cyphar/filepath-securejoin # github.com/docker/go-units v0.4.0