diff --git a/libcontainer/configs/mount_linux.go b/libcontainer/configs/mount_linux.go index 5cb7883c9..a4275df3a 100644 --- a/libcontainer/configs/mount_linux.go +++ b/libcontainer/configs/mount_linux.go @@ -29,8 +29,24 @@ type Mount struct { // Extensions are additional flags that are specific to runc. Extensions int `json:"extensions"` + + // UIDMappings is used to changing file user owners w/o calling chown. + // Note that, the underlying filesystem should support this feature to be + // used. + // Every mount point could have its own mapping. + UIDMappings []IDMap `json:"uidMappings,omitempty"` + + // GIDMappings is used to changing file group owners w/o calling chown. + // Note that, the underlying filesystem should support this feature to be + // used. + // Every mount point could have its own mapping. + GIDMappings []IDMap `json:"gidMappings,omitempty"` } func (m *Mount) IsBind() bool { return m.Flags&unix.MS_BIND != 0 } + +func (m *Mount) IsIDMapped() bool { + return len(m.UIDMappings) > 0 || len(m.GIDMappings) > 0 +} diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go index 26e137901..5f065a384 100644 --- a/libcontainer/configs/validate/validator.go +++ b/libcontainer/configs/validate/validator.go @@ -253,16 +253,72 @@ func cgroupsCheck(config *configs.Config) error { return nil } +func checkIDMapMounts(config *configs.Config, m *configs.Mount) error { + if !m.IsIDMapped() { + return nil + } + + if !m.IsBind() { + return fmt.Errorf("gidMappings/uidMappings is supported only for mounts with the option 'bind'") + } + if config.RootlessEUID { + return fmt.Errorf("gidMappings/uidMappings is not supported when runc is being launched with EUID != 0, needs CAP_SYS_ADMIN on the runc parent's user namespace") + } + if len(config.UidMappings) == 0 || len(config.GidMappings) == 0 { + return fmt.Errorf("not yet supported to use gidMappings/uidMappings in a mount without also using a user namespace") + } + if !sameMapping(config.UidMappings, m.UIDMappings) { + return fmt.Errorf("not yet supported for the mount uidMappings to be different than user namespace uidMapping") + } + if !sameMapping(config.GidMappings, m.GIDMappings) { + return fmt.Errorf("not yet supported for the mount gidMappings to be different than user namespace gidMapping") + } + if !filepath.IsAbs(m.Source) { + return fmt.Errorf("mount source not absolute") + } + + return nil +} + func mounts(config *configs.Config) error { for _, m := range config.Mounts { + // We upgraded this to an error in runc 1.2. We might need to + // revert this change if some users haven't still moved to use + // abs paths, in that please move this check inside + // checkIDMapMounts() as we do want to ensure that for idmap + // mounts anyways. if !filepath.IsAbs(m.Destination) { return fmt.Errorf("invalid mount %+v: mount destination not absolute", m) } + if err := checkIDMapMounts(config, m); err != nil { + return fmt.Errorf("invalid mount %+v: %w", m, err) + } } return nil } +// sameMapping checks if the mappings are the same. If the mappings are the same +// but in different order, it returns false. +func sameMapping(a, b []configs.IDMap) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i].ContainerID != b[i].ContainerID { + return false + } + if a[i].HostID != b[i].HostID { + return false + } + if a[i].Size != b[i].Size { + return false + } + } + return true +} + func isHostNetNS(path string) (bool, error) { const currentProcessNetns = "/proc/self/ns/net" diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go index ad632c89f..fc92a7bb8 100644 --- a/libcontainer/configs/validate/validator_test.go +++ b/libcontainer/configs/validate/validator_test.go @@ -386,3 +386,199 @@ func TestValidateMounts(t *testing.T) { } } } + +func TestValidateIDMapMounts(t *testing.T) { + mapping := []configs.IDMap{ + { + ContainerID: 0, + HostID: 10000, + Size: 1, + }, + } + + testCases := []struct { + name string + isErr bool + config *configs.Config + }{ + { + name: "idmap mount without bind opt specified", + isErr: true, + config: &configs.Config{ + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "/abs/path/", + Destination: "/abs/path/", + UIDMappings: mapping, + GIDMappings: mapping, + }, + }, + }, + }, + { + name: "rootless idmap mount", + isErr: true, + config: &configs.Config{ + RootlessEUID: true, + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "/abs/path/", + Destination: "/abs/path/", + Flags: unix.MS_BIND, + UIDMappings: mapping, + GIDMappings: mapping, + }, + }, + }, + }, + { + name: "idmap mount without userns mappings", + isErr: true, + config: &configs.Config{ + Mounts: []*configs.Mount{ + { + Source: "/abs/path/", + Destination: "/abs/path/", + Flags: unix.MS_BIND, + UIDMappings: mapping, + GIDMappings: mapping, + }, + }, + }, + }, + { + name: "idmap mounts with different userns and mount mappings", + isErr: true, + config: &configs.Config{ + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "/abs/path/", + Destination: "/abs/path/", + Flags: unix.MS_BIND, + UIDMappings: []configs.IDMap{ + { + ContainerID: 10, + HostID: 10, + Size: 1, + }, + }, + GIDMappings: mapping, + }, + }, + }, + }, + { + name: "idmap mounts with different userns and mount mappings", + isErr: true, + config: &configs.Config{ + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "/abs/path/", + Destination: "/abs/path/", + Flags: unix.MS_BIND, + UIDMappings: mapping, + GIDMappings: []configs.IDMap{ + { + ContainerID: 10, + HostID: 10, + Size: 1, + }, + }, + }, + }, + }, + }, + { + name: "idmap mounts without abs source path", + isErr: true, + config: &configs.Config{ + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "./rel/path/", + Destination: "/abs/path/", + Flags: unix.MS_BIND, + UIDMappings: mapping, + GIDMappings: mapping, + }, + }, + }, + }, + { + name: "idmap mounts without abs dest path", + isErr: true, + config: &configs.Config{ + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "/abs/path/", + Destination: "./rel/path/", + Flags: unix.MS_BIND, + UIDMappings: mapping, + GIDMappings: mapping, + }, + }, + }, + }, + + { + name: "simple idmap mount", + config: &configs.Config{ + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "/another-abs/path/", + Destination: "/abs/path/", + Flags: unix.MS_BIND, + UIDMappings: mapping, + GIDMappings: mapping, + }, + }, + }, + }, + { + name: "idmap mount with more flags", + config: &configs.Config{ + UidMappings: mapping, + GidMappings: mapping, + Mounts: []*configs.Mount{ + { + Source: "/another-abs/path/", + Destination: "/abs/path/", + Flags: unix.MS_BIND | unix.MS_RDONLY, + UIDMappings: mapping, + GIDMappings: mapping, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + config := tc.config + config.Rootfs = "/var" + + err := mounts(config) + if tc.isErr && err == nil { + t.Error("expected error, got nil") + } + + if !tc.isErr && err != nil { + t.Error(err) + } + }) + } +} diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index eac17027a..e608d7609 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -531,9 +531,9 @@ func (c *Container) shouldSendMountSources() bool { return false } - // We need to send sources if there are bind-mounts. + // We need to send sources if there are non-idmap bind-mounts. for _, m := range c.config.Mounts { - if m.IsBind() { + if m.IsBind() && !m.IsIDMapped() { return true } } @@ -2231,7 +2231,7 @@ func (c *Container) bootstrapData(cloneFlags uintptr, nsMaps map[configs.Namespa if it == initStandard && c.shouldSendMountSources() { var mounts []byte for _, m := range c.config.Mounts { - if m.IsBind() { + if m.IsBind() && !m.IsIDMapped() { if strings.IndexByte(m.Source, 0) >= 0 { return nil, fmt.Errorf("mount source string contains null byte: %q", m.Source) } diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 809424a97..cda29f414 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -497,6 +497,18 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { return config, nil } +func toConfigIDMap(specMaps []specs.LinuxIDMapping) []configs.IDMap { + idmaps := make([]configs.IDMap, len(specMaps)) + for i, id := range specMaps { + idmaps[i] = configs.IDMap{ + ContainerID: int(id.ContainerID), + HostID: int(id.HostID), + Size: int(id.Size), + } + } + return idmaps +} + func createLibcontainerMount(cwd string, m specs.Mount) (*configs.Mount, error) { if !filepath.IsAbs(m.Destination) { // Relax validation for backward compatibility @@ -519,6 +531,9 @@ func createLibcontainerMount(cwd string, m specs.Mount) (*configs.Mount, error) } } + mnt.UIDMappings = toConfigIDMap(m.UIDMappings) + mnt.GIDMappings = toConfigIDMap(m.GIDMappings) + // None of the mount arguments can contain a null byte. Normally such // strings would either cause some other failure or would just be truncated // when we hit the null byte, but because we serialise these strings as