Add uid and gid mappings to mounts

Co-authored-by: Francis Laniel <flaniel@linux.microsoft.com>
Signed-off-by: Rodrigo Campos <rodrigoca@microsoft.com>
This commit is contained in:
Rodrigo Campos
2023-04-11 11:23:26 +02:00
parent 881e92a3fd
commit fbf183c6f8
5 changed files with 286 additions and 3 deletions

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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)
}
})
}
}

View File

@@ -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)
}

View File

@@ -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