From e835e87867b5511e2909210259c12e3d9444b404 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 15 Jan 2018 16:09:03 +0900 Subject: [PATCH] rootless: optional support for generating config with subuid map Signed-off-by: Akihiro Suda --- libcontainer/specconv/example.go | 139 +++++++++++++++-- libcontainer/specconv/spec_linux_test.go | 189 ++++++++++++++++++++--- spec.go | 11 +- 3 files changed, 308 insertions(+), 31 deletions(-) diff --git a/libcontainer/specconv/example.go b/libcontainer/specconv/example.go index 46c5640e47f..d7b4f56aa64 100644 --- a/libcontainer/specconv/example.go +++ b/libcontainer/specconv/example.go @@ -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" ) @@ -155,10 +158,60 @@ func Example() *specs.Spec { } } +// RootlessOpts is an optional spec for ToRootless +type RootlessOpts struct { + // Add sub{u,g}id to spec.Linux.{U,G}IDMappings. + // Requires newuidmap(1) and newgidmap(1) with suid bit. + // Ignored when running in userns. + MapSubUIDGID bool +} + +// 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 +} + // ExampleRootless returns an example spec file that works with rootless // containers. It's essentially a modified version of the specfile from // Example(). -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. @@ -177,16 +230,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 := 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 := 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.MapSubUIDGID 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.MapSubUIDGID { + uNextContainerID := 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 := 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 @@ -218,4 +321,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 } diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go index 9b90efb2832..8fa061942f2 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -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" ) @@ -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{ + MapSubUIDGID: 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{ + MapSubUIDGID: 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) + } } } diff --git a/spec.go b/spec.go index 26e9754ef19..fe4b46cba22 100644 --- a/spec.go +++ b/spec.go @@ -72,6 +72,10 @@ generate a proper rootless spec file.`, Name: "rootless", Usage: "generate a configuration for a rootless container", }, + cli.BoolFlag{ + Name: "rootless-subuid", + Usage: "add subuid and subgid entries to the mapping configuration. requires rootless to be true and newuidmap/newgidmap binaries with suid bit. ignored when running in userns.", + }, }, Action: func(context *cli.Context) error { if err := checkArgs(context, 0, exactArgs); err != nil { @@ -81,7 +85,12 @@ generate a proper rootless spec file.`, rootless := context.Bool("rootless") if rootless { - specconv.ToRootless(spec) + opts := &specconv.RootlessOpts{ + MapSubUIDGID: context.Bool("rootless-subuid"), + } + if err := specconv.ToRootless(spec, opts); err != nil { + return err + } } checkNoFile := func(name string) error {