From 298d6cdd742242ca9496320985314aa97532736d 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 | 36 +++++++++++++- libcontainer/specconv/spec_linux_test.go | 34 +++++++------ libcontainer/user/lookup.go | 2 + libcontainer/user/lookup_unix.go | 32 +++++++++++++ libcontainer/user/user.go | 61 ++++++++++++++++++++++++ spec.go | 11 ++++- 6 files changed, 160 insertions(+), 16 deletions(-) diff --git a/libcontainer/specconv/example.go b/libcontainer/specconv/example.go index 46c5640e47f..058bf52f0cd 100644 --- a/libcontainer/specconv/example.go +++ b/libcontainer/specconv/example.go @@ -4,6 +4,7 @@ import ( "os" "strings" + "github.com/opencontainers/runc/libcontainer/user" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -155,10 +156,20 @@ 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. + MapSubUIDGID 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 { + if opts == nil { + opts = &RootlessOpts{} + } var namespaces []specs.LinuxNamespace // Remove networkns from the spec. @@ -187,6 +198,28 @@ func ToRootless(spec *specs.Spec) { ContainerID: 0, Size: 1, }} + if opts.MapSubUIDGID { + subuid, err := user.CurrentSubUID() + if err != nil { + return err + } + spec.Linux.UIDMappings = append(spec.Linux.UIDMappings, + specs.LinuxIDMapping{ + HostID: uint32(subuid.SubID), + ContainerID: 1, + Size: uint32(subuid.Count), + }) + subgid, err := user.CurrentSubGID() + if err != nil { + return err + } + spec.Linux.GIDMappings = append(spec.Linux.GIDMappings, + specs.LinuxIDMapping{ + HostID: uint32(subgid.SubID), + ContainerID: 1, + Size: uint32(subgid.Count), + }) + } // Fix up mounts. var mounts []specs.Mount @@ -218,4 +251,5 @@ func ToRootless(spec *specs.Spec) { // Remove cgroup settings. spec.Linux.Resources = nil + return nil } diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go index 9b90efb2832..67b87d0670f 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -424,22 +424,28 @@ func TestRootlessSpecconvValidate(t *testing.T) { spec := Example() spec.Root.Path = "/" - ToRootless(spec) + optsCases := []*RootlessOpts{nil, {MapSubUIDGID: true}} + for _, opts := range optsCases { + err := ToRootless(spec, 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, - } + opts := &CreateOpts{ + CgroupName: "ContainerID", + UseSystemdCgroup: false, + Spec: spec, + Rootless: true, + } - config, err := CreateLibcontainerConfig(opts) - if err != nil { - t.Errorf("Couldn't create libcontainer 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) + 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/libcontainer/user/lookup.go b/libcontainer/user/lookup.go index 95e9eebc0b0..c0d1e6ad944 100644 --- a/libcontainer/user/lookup.go +++ b/libcontainer/user/lookup.go @@ -10,6 +10,8 @@ var ( // No matching entries found in file. ErrNoPasswdEntries = errors.New("no matching entries in passwd file") ErrNoGroupEntries = errors.New("no matching entries in group file") + ErrNoSubUIDEntries = errors.New("no matching entries in subuid file") + ErrNoSubGIDEntries = errors.New("no matching entries in subgid file") ) func lookupUser(filter func(u User) bool) (User, error) { diff --git a/libcontainer/user/lookup_unix.go b/libcontainer/user/lookup_unix.go index c2bb9ec90da..755800ff929 100644 --- a/libcontainer/user/lookup_unix.go +++ b/libcontainer/user/lookup_unix.go @@ -44,3 +44,35 @@ func CurrentUser() (User, error) { func CurrentGroup() (Group, error) { return LookupGid(unix.Getgid()) } + +func CurrentSubUID() (SubID, error) { + u, err := CurrentUser() + if err != nil { + return SubID{}, err + } + entries, err := ParseSubIDFileFilter("/etc/subuid", + func(entry SubID) bool { return entry.Name == u.Name }) + if err != nil { + return SubID{}, err + } + if len(entries) == 0 { + return SubID{}, ErrNoSubUIDEntries + } + return entries[0], nil +} + +func CurrentSubGID() (SubID, error) { + g, err := CurrentGroup() + if err != nil { + return SubID{}, err + } + entries, err := ParseSubIDFileFilter("/etc/subgid", + func(entry SubID) bool { return entry.Name == g.Name }) + if err != nil { + return SubID{}, err + } + if len(entries) == 0 { + return SubID{}, ErrNoSubGIDEntries + } + return entries[0], nil +} diff --git a/libcontainer/user/user.go b/libcontainer/user/user.go index 8962cab331e..5f9360d29b1 100644 --- a/libcontainer/user/user.go +++ b/libcontainer/user/user.go @@ -35,6 +35,13 @@ type Group struct { List []string } +// SubID represents an entry in /etc/sub{u,g}id +type SubID struct { + Name string + SubID int + Count int +} + func parseLine(line string, v ...interface{}) { if line == "" { return @@ -439,3 +446,57 @@ func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int } return GetAdditionalGroups(additionalGroups, group) } + +func ParseSubIDFile(path string) ([]SubID, error) { + subid, err := os.Open(path) + if err != nil { + return nil, err + } + defer subid.Close() + return ParseSubID(subid) +} + +func ParseSubID(subid io.Reader) ([]SubID, error) { + return ParseSubIDFilter(subid, nil) +} + +func ParseSubIDFileFilter(path string, filter func(SubID) bool) ([]SubID, error) { + subid, err := os.Open(path) + if err != nil { + return nil, err + } + defer subid.Close() + return ParseSubIDFilter(subid, filter) +} + +func ParseSubIDFilter(r io.Reader, filter func(SubID) bool) ([]SubID, error) { + if r == nil { + return nil, fmt.Errorf("nil source for subid-formatted data") + } + + var ( + s = bufio.NewScanner(r) + out = []SubID{} + ) + + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + + line := strings.TrimSpace(s.Text()) + if line == "" { + continue + } + + // see: man 5 subuid + p := SubID{} + parseLine(line, &p.Name, &p.SubID, &p.Count) + + if filter == nil || filter(p) { + out = append(out, p) + } + } + + return out, nil +} diff --git a/spec.go b/spec.go index 26e9754ef19..45f02729a4b 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.", + }, }, 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 {