Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rootless: optional support for generating config with subuid map #1692

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 134 additions & 11 deletions libcontainer/specconv/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package specconv

import (
"os"
"sort"
"strings"

"github.com/opencontainers/runc/libcontainer/system"
"github.com/opencontainers/runc/libcontainer/user"
"github.com/opencontainers/runtime-spec/specs-go"
)

Expand Down Expand Up @@ -155,10 +158,66 @@ func Example() *specs.Spec {
}
}

// RootlessOpts is an optional spec for ToRootless
type RootlessOpts struct {
// Add all subuids/subgids to spec.Linux.{U,G}IDMappings.
// Note that in many cases users shouldn't be mapping all of their
// allocated subuids/subgids for each container.
// They should be using independent sets of uids and gids if possible.
//
// MapAllSubIDs requires newuidmap(1) and newgidmap(1) with suid bit.
//
// When running in userns, MapAllSubIDs is ignored and
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is very confusing, I'm temporary closing this PR.

Please also see #1837

// /proc/self/[ug]id_map entries are used.
MapAllSubIDs bool
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to add more options such as KeepNetworkNamespace as well after this PR gets merged.

}

// Run-time context for ToRootless.
type RootlessContext struct {
EUID uint32
EGID uint32
SubUIDs []user.SubID
SubGIDs []user.SubID
UIDMap []user.IDMap
GIDMap []user.IDMap
InUserNS bool
}

// ToRootless converts the given spec file into one that should work with
// rootless containers, by removing incompatible options and adding others that
// are needed.
func ToRootless(spec *specs.Spec) {
func ToRootless(spec *specs.Spec, opts *RootlessOpts) error {
var err error
ctx := RootlessContext{}
ctx.EUID = uint32(os.Geteuid())
ctx.EGID = uint32(os.Getegid())
ctx.SubUIDs, err = user.CurrentUserSubUIDs()
if err != nil && !os.IsNotExist(err) {
return err
}
ctx.SubGIDs, err = user.CurrentGroupSubGIDs()
if err != nil && !os.IsNotExist(err) {
return err
}
ctx.UIDMap, err = user.CurrentProcessUIDMap()
if err != nil && !os.IsNotExist(err) {
return err
}
uidMapExists := !os.IsNotExist(err)
ctx.GIDMap, err = user.CurrentProcessUIDMap()
if err != nil && !os.IsNotExist(err) {
return err
}
ctx.InUserNS = uidMapExists && system.UIDMapInUserNS(ctx.UIDMap)
return ToRootlessWithContext(ctx, spec, opts)
}

// ToRootlessWithContext converts the spec with the run-time context.
// ctx can be internally modified for sorting.
func ToRootlessWithContext(ctx RootlessContext, spec *specs.Spec, opts *RootlessOpts) error {
if opts == nil {
opts = &RootlessOpts{}
}
var namespaces []specs.LinuxNamespace

// Remove networkns from the spec.
Expand All @@ -177,16 +236,66 @@ func ToRootless(spec *specs.Spec) {
spec.Linux.Namespaces = namespaces

// Add mappings for the current user.
spec.Linux.UIDMappings = []specs.LinuxIDMapping{{
HostID: uint32(os.Geteuid()),
ContainerID: 0,
Size: 1,
}}
spec.Linux.GIDMappings = []specs.LinuxIDMapping{{
HostID: uint32(os.Getegid()),
ContainerID: 0,
Size: 1,
}}
if ctx.InUserNS {
uNextContainerID := int64(0)
sort.Sort(idmapSorter(ctx.UIDMap))
for _, uidmap := range ctx.UIDMap {
spec.Linux.UIDMappings = append(spec.Linux.UIDMappings,
specs.LinuxIDMapping{
HostID: uint32(uidmap.ID),
ContainerID: uint32(uNextContainerID),
Size: uint32(uidmap.Count),
})
uNextContainerID += uidmap.Count
}
gNextContainerID := int64(0)
sort.Sort(idmapSorter(ctx.GIDMap))
for _, gidmap := range ctx.GIDMap {
spec.Linux.GIDMappings = append(spec.Linux.GIDMappings,
specs.LinuxIDMapping{
HostID: uint32(gidmap.ID),
ContainerID: uint32(gNextContainerID),
Size: uint32(gidmap.Count),
})
gNextContainerID += gidmap.Count
}
// opts.MapAllSubIDs is ignored in userns
} else {
spec.Linux.UIDMappings = []specs.LinuxIDMapping{{
HostID: ctx.EUID,
ContainerID: 0,
Size: 1,
}}
spec.Linux.GIDMappings = []specs.LinuxIDMapping{{
HostID: ctx.EGID,
ContainerID: 0,
Size: 1,
}}
if opts.MapAllSubIDs {
uNextContainerID := int64(1)
sort.Sort(subIDSorter(ctx.SubUIDs))
for _, subuid := range ctx.SubUIDs {
spec.Linux.UIDMappings = append(spec.Linux.UIDMappings,
specs.LinuxIDMapping{
HostID: uint32(subuid.SubID),
ContainerID: uint32(uNextContainerID),
Size: uint32(subuid.Count),
})
uNextContainerID += subuid.Count
}
gNextContainerID := int64(1)
sort.Sort(subIDSorter(ctx.SubGIDs))
for _, subgid := range ctx.SubGIDs {
spec.Linux.GIDMappings = append(spec.Linux.GIDMappings,
specs.LinuxIDMapping{
HostID: uint32(subgid.SubID),
ContainerID: uint32(gNextContainerID),
Size: uint32(subgid.Count),
})
gNextContainerID += subgid.Count
}
}
}

// Fix up mounts.
var mounts []specs.Mount
Expand Down Expand Up @@ -218,4 +327,18 @@ func ToRootless(spec *specs.Spec) {

// Remove cgroup settings.
spec.Linux.Resources = nil
return nil
}

// subIDSorter is required for Go <= 1.7
type subIDSorter []user.SubID

func (x subIDSorter) Len() int { return len(x) }
func (x subIDSorter) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x subIDSorter) Less(i, j int) bool { return x[i].SubID < x[j].SubID }

type idmapSorter []user.IDMap

func (x idmapSorter) Len() int { return len(x) }
func (x idmapSorter) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x idmapSorter) Less(i, j int) bool { return x[i].ID < x[j].ID }
189 changes: 170 additions & 19 deletions libcontainer/specconv/spec_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
package specconv

import (
"os"
"reflect"
"testing"

"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/configs/validate"
"github.com/opencontainers/runc/libcontainer/user"
"github.com/opencontainers/runtime-spec/specs-go"
)

Expand Down Expand Up @@ -418,28 +419,178 @@ func TestDupNamespaces(t *testing.T) {
}

func TestRootlessSpecconvValidate(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
t.Skip("userns is unsupported")
specGen := func() *specs.Spec {
spec := Example()
spec.Root.Path = "/"
return spec
}
cases := []struct {
ctx RootlessContext
opts *RootlessOpts
additionalValidator func(t *testing.T, s *specs.Spec)
}{
{
ctx: RootlessContext{
EUID: 0,
EGID: 0,
},
},
{
ctx: RootlessContext{
EUID: 4242,
EGID: 4242,
},
},
{
ctx: RootlessContext{
EUID: 4242,
EGID: 4242,
// empty subuid / subgid
},
opts: &RootlessOpts{
MapAllSubIDs: true,
},
},
{
ctx: RootlessContext{
EUID: 4242,
EGID: 4242,
SubUIDs: []user.SubID{
{
Name: "dummy",
SubID: 14242,
Count: 65536,
},
{
Name: "dummy",
SubID: 114242,
Count: 65536,
},
},
SubGIDs: []user.SubID{
{
Name: "dummy",
SubID: 14242,
Count: 65536,
},
{
Name: "dummy",
SubID: 114242,
Count: 65536,
},
},
},
opts: &RootlessOpts{
MapAllSubIDs: true,
},
additionalValidator: func(t *testing.T, s *specs.Spec) {
expectedUIDMappings := []specs.LinuxIDMapping{
{
HostID: 4242,
ContainerID: 0,
Size: 1,
},
{
HostID: 14242,
ContainerID: 1,
Size: 65536,
},
{
HostID: 114242,
ContainerID: 65537,
Size: 65536,
},
}
if !reflect.DeepEqual(expectedUIDMappings, s.Linux.UIDMappings) {
t.Errorf("expected %#v, got %#v", expectedUIDMappings, s.Linux.UIDMappings)
}
expectedGIDMappings := expectedUIDMappings
if !reflect.DeepEqual(expectedGIDMappings, s.Linux.GIDMappings) {
t.Errorf("expected %#v, got %#v", expectedGIDMappings, s.Linux.GIDMappings)
}
},
},
{
ctx: RootlessContext{
EUID: 0,
EGID: 0,
UIDMap: []user.IDMap{
{
ID: 0,
ParentID: 4242,
Count: 1,
},
{
ID: 1,
ParentID: 231072,
Count: 65536,
},
},
GIDMap: []user.IDMap{
{
ID: 0,
ParentID: 4242,
Count: 1,
},
{
ID: 1,
ParentID: 231072,
Count: 65536,
},
},
InUserNS: true,
},
additionalValidator: func(t *testing.T, s *specs.Spec) {
expectedUIDMappings := []specs.LinuxIDMapping{
{
HostID: 0,
ContainerID: 0,
Size: 1,
},
{
HostID: 1,
ContainerID: 1,
Size: 65536,
},
}
if !reflect.DeepEqual(expectedUIDMappings, s.Linux.UIDMappings) {
t.Errorf("expected %#v, got %#v", expectedUIDMappings, s.Linux.UIDMappings)
}
expectedGIDMappings := expectedUIDMappings
if !reflect.DeepEqual(expectedGIDMappings, s.Linux.GIDMappings) {
t.Errorf("expected %#v, got %#v", expectedGIDMappings, s.Linux.GIDMappings)
}
},
},
}

spec := Example()
spec.Root.Path = "/"
ToRootless(spec)
for _, c := range cases {
spec := specGen()
err := ToRootlessWithContext(c.ctx, spec, c.opts)
if err != nil {
t.Errorf("Couldn't convert a rootful spec to rootless: %v", err)
}

opts := &CreateOpts{
CgroupName: "ContainerID",
UseSystemdCgroup: false,
Spec: spec,
Rootless: true,
}
// t.Logf("%#v", spec)
if c.additionalValidator != nil {
c.additionalValidator(t, spec)
}

config, err := CreateLibcontainerConfig(opts)
if err != nil {
t.Errorf("Couldn't create libcontainer config: %v", err)
}
opts := &CreateOpts{
CgroupName: "ContainerID",
UseSystemdCgroup: false,
Spec: spec,
Rootless: true,
}

validator := validate.New()
if err := validator.Validate(config); err != nil {
t.Errorf("Expected specconv to produce valid rootless container config: %v", err)
config, err := CreateLibcontainerConfig(opts)
if err != nil {
t.Errorf("Couldn't create libcontainer config: %v", err)
}

validator := validate.New()
if err := validator.Validate(config); err != nil {
t.Errorf("Expected specconv to produce valid rootless container config: %v", err)
}
}
}
5 changes: 4 additions & 1 deletion spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ generate a proper rootless spec file.`,

rootless := context.Bool("rootless")
if rootless {
specconv.ToRootless(spec)
opts := &specconv.RootlessOpts{}
if err := specconv.ToRootless(spec, opts); err != nil {
return err
}
}

checkNoFile := func(name string) error {
Expand Down