Skip to content

Commit

Permalink
Add uid and gid mappings to mounts
Browse files Browse the repository at this point in the history
Co-authored-by: Francis Laniel <[email protected]>
Signed-off-by: Rodrigo Campos <[email protected]>
  • Loading branch information
rata and eiffel-fl committed Jul 11, 2023
1 parent 881e92a commit fbf183c
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 3 deletions.
16 changes: 16 additions & 0 deletions libcontainer/configs/mount_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
56 changes: 56 additions & 0 deletions libcontainer/configs/validate/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
196 changes: 196 additions & 0 deletions libcontainer/configs/validate/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
6 changes: 3 additions & 3 deletions libcontainer/container_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
15 changes: 15 additions & 0 deletions libcontainer/specconv/spec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit fbf183c

Please sign in to comment.