From 9c7d8bc1fd1b5c0903c5f2c94d5a84b0d6f7fb18 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 16 Jan 2018 15:11:56 +0900 Subject: [PATCH 1/4] libcontainer: add parser for /etc/sub{u,g}id and /proc/PID/{u,g}id_map Signed-off-by: Akihiro Suda --- libcontainer/system/linux.go | 23 ++--- libcontainer/system/linux_test.go | 45 ++++++++++ libcontainer/system/unsupported.go | 8 ++ libcontainer/user/lookup_unix.go | 26 ++++++ libcontainer/user/user.go | 129 ++++++++++++++++++++++++++++- 5 files changed, 213 insertions(+), 18 deletions(-) create mode 100644 libcontainer/system/linux_test.go diff --git a/libcontainer/system/linux.go b/libcontainer/system/linux.go index 5f124cd8b..70260ae23 100644 --- a/libcontainer/system/linux.go +++ b/libcontainer/system/linux.go @@ -3,13 +3,11 @@ package system import ( - "bufio" - "fmt" - "os" "os/exec" "syscall" // only for exec "unsafe" + "github.com/opencontainers/runc/libcontainer/user" "golang.org/x/sys/unix" ) @@ -102,29 +100,22 @@ func Setctty() error { } // RunningInUserNS detects whether we are currently running in a user namespace. -// Copied from github.com/lxc/lxd/shared/util.go +// Originally copied from github.com/lxc/lxd/shared/util.go func RunningInUserNS() bool { - file, err := os.Open("/proc/self/uid_map") + uidmap, err := user.CurrentProcessUIDMap() if err != nil { // This kernel-provided file only exists if user namespaces are supported return false } - defer file.Close() + return UIDMapInUserNS(uidmap) +} - buf := bufio.NewReader(file) - l, _, err := buf.ReadLine() - if err != nil { - return false - } - - line := string(l) - var a, b, c int64 - fmt.Sscanf(line, "%d %d %d", &a, &b, &c) +func UIDMapInUserNS(uidmap []user.IDMap) bool { /* * We assume we are in the initial user namespace if we have a full * range - 4294967295 uids starting at uid 0. */ - if a == 0 && b == 0 && c == 4294967295 { + if len(uidmap) == 1 && uidmap[0].ID == 0 && uidmap[0].ParentID == 0 && uidmap[0].Count == 4294967295 { return false } return true diff --git a/libcontainer/system/linux_test.go b/libcontainer/system/linux_test.go new file mode 100644 index 000000000..4d613d847 --- /dev/null +++ b/libcontainer/system/linux_test.go @@ -0,0 +1,45 @@ +// +build linux + +package system + +import ( + "strings" + "testing" + + "github.com/opencontainers/runc/libcontainer/user" +) + +func TestUIDMapInUserNS(t *testing.T) { + cases := []struct { + s string + expected bool + }{ + { + s: " 0 0 4294967295\n", + expected: false, + }, + { + s: " 0 0 1\n", + expected: true, + }, + { + s: " 0 1001 1\n 1 231072 65536\n", + expected: true, + }, + { + // file exist but empty (the initial state when userns is created. see man 7 user_namespaces) + s: "", + expected: true, + }, + } + for _, c := range cases { + uidmap, err := user.ParseIDMap(strings.NewReader(c.s)) + if err != nil { + t.Fatal(err) + } + actual := UIDMapInUserNS(uidmap) + if c.expected != actual { + t.Fatalf("expected %v, got %v for %q", c.expected, actual, c.s) + } + } +} diff --git a/libcontainer/system/unsupported.go b/libcontainer/system/unsupported.go index e7cfd62b2..a8bea3022 100644 --- a/libcontainer/system/unsupported.go +++ b/libcontainer/system/unsupported.go @@ -2,8 +2,16 @@ package system +import "github.com/opencontainers/runc/libcontainer/user" + // RunningInUserNS is a stub for non-Linux systems // Always returns false func RunningInUserNS() bool { return false } + +// UIDMapInUserNS is a stub for non-Linux systems +// Always returns false +func UIDMapInUserNS(uidmap []user.IDMap) bool { + return false +} diff --git a/libcontainer/user/lookup_unix.go b/libcontainer/user/lookup_unix.go index c45e30041..c1e634c94 100644 --- a/libcontainer/user/lookup_unix.go +++ b/libcontainer/user/lookup_unix.go @@ -114,3 +114,29 @@ func CurrentUser() (User, error) { func CurrentGroup() (Group, error) { return LookupGid(unix.Getgid()) } + +func CurrentUserSubUIDs() ([]SubID, error) { + u, err := CurrentUser() + if err != nil { + return nil, err + } + return ParseSubIDFileFilter("/etc/subuid", + func(entry SubID) bool { return entry.Name == u.Name }) +} + +func CurrentGroupSubGIDs() ([]SubID, error) { + g, err := CurrentGroup() + if err != nil { + return nil, err + } + return ParseSubIDFileFilter("/etc/subgid", + func(entry SubID) bool { return entry.Name == g.Name }) +} + +func CurrentProcessUIDMap() ([]IDMap, error) { + return ParseIDMapFile("/proc/self/uid_map") +} + +func CurrentProcessGIDMap() ([]IDMap, error) { + return ParseIDMapFile("/proc/self/gid_map") +} diff --git a/libcontainer/user/user.go b/libcontainer/user/user.go index 93414516c..37993da83 100644 --- a/libcontainer/user/user.go +++ b/libcontainer/user/user.go @@ -75,12 +75,29 @@ func groupFromOS(g *user.Group) (Group, error) { return newGroup, nil } +// SubID represents an entry in /etc/sub{u,g}id +type SubID struct { + Name string + SubID int + Count int +} + +// IDMap represents an entry in /proc/PID/{u,g}id_map +type IDMap struct { + ID int + ParentID int + Count int +} + func parseLine(line string, v ...interface{}) { - if line == "" { + parseParts(strings.Split(line, ":"), v...) +} + +func parseParts(parts []string, v ...interface{}) { + if len(parts) == 0 { return } - parts := strings.Split(line, ":") for i, p := range parts { // Ignore cases where we don't have enough fields to populate the arguments. // Some configuration files like to misbehave. @@ -479,3 +496,111 @@ 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 +} + +func ParseIDMapFile(path string) ([]IDMap, error) { + r, err := os.Open(path) + if err != nil { + return nil, err + } + defer r.Close() + return ParseIDMap(r) +} + +func ParseIDMap(r io.Reader) ([]IDMap, error) { + return ParseIDMapFilter(r, nil) +} + +func ParseIDMapFileFilter(path string, filter func(IDMap) bool) ([]IDMap, error) { + r, err := os.Open(path) + if err != nil { + return nil, err + } + defer r.Close() + return ParseIDMapFilter(r, filter) +} + +func ParseIDMapFilter(r io.Reader, filter func(IDMap) bool) ([]IDMap, error) { + if r == nil { + return nil, fmt.Errorf("nil source for idmap-formatted data") + } + + var ( + s = bufio.NewScanner(r) + out = []IDMap{} + ) + + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + + line := strings.TrimSpace(s.Text()) + if line == "" { + continue + } + + // see: man 7 user_namespaces + p := IDMap{} + parseParts(strings.Fields(line), &p.ID, &p.ParentID, &p.Count) + + if filter == nil || filter(p) { + out = append(out, p) + } + } + + return out, nil +} From f103de57ec89137d86b77584f676b760ec7c7ba4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 13 Jan 2018 16:39:28 +0900 Subject: [PATCH 2/4] main: support rootless mode in userns Running rootless containers in userns is useful for mounting filesystems (e.g. overlay) with mapped euid 0, but without actual root privilege. Usage: (Note that `unshare --mount` requires `--map-root-user`) user$ mkdir lower upper work rootfs user$ curl http://dl-cdn.alpinelinux.org/alpine/v3.7/releases/x86_64/alpine-minirootfs-3.7.0-x86_64.tar.gz | tar Cxz ./lower || ( true; echo "mknod errors were ignored" ) user$ unshare --mount --map-root-user mappedroot# runc spec --rootless mappedroot# sed -i 's/"readonly": true/"readonly": false/g' config.json mappedroot# mount -t overlay -o lowerdir=./lower,upperdir=./upper,workdir=./work overlayfs ./rootfs mappedroot# runc run foo Signed-off-by: Akihiro Suda --- checkpoint.go | 6 +++++- libcontainer/system/linux.go | 17 +++++++++++++++++ libcontainer/system/unsupported.go | 12 +++++++++++- main.go | 11 ++++++++++- ps.go | 6 +++++- restore.go | 6 +++++- utils.go | 11 +++++++++++ utils_linux.go | 27 +++++++++++++++++++++++---- 8 files changed, 87 insertions(+), 9 deletions(-) diff --git a/checkpoint.go b/checkpoint.go index e7c126743..61e58351f 100644 --- a/checkpoint.go +++ b/checkpoint.go @@ -44,7 +44,11 @@ checkpointed.`, return err } // XXX: Currently this is untested with rootless containers. - if isRootless() { + rootless, err := isRootless(context) + if err != nil { + return err + } + if rootless { return fmt.Errorf("runc checkpoint requires root") } diff --git a/libcontainer/system/linux.go b/libcontainer/system/linux.go index 70260ae23..8d353d984 100644 --- a/libcontainer/system/linux.go +++ b/libcontainer/system/linux.go @@ -3,6 +3,7 @@ package system import ( + "os" "os/exec" "syscall" // only for exec "unsafe" @@ -121,6 +122,22 @@ func UIDMapInUserNS(uidmap []user.IDMap) bool { return true } +// GetParentNSeuid returns the euid within the parent user namespace +func GetParentNSeuid() int { + euid := os.Geteuid() + uidmap, err := user.CurrentProcessUIDMap() + if err != nil { + // This kernel-provided file only exists if user namespaces are supported + return euid + } + for _, um := range uidmap { + if um.ID <= euid && euid <= um.ID+um.Count-1 { + return um.ParentID + euid - um.ID + } + } + return euid +} + // SetSubreaper sets the value i as the subreaper setting for the calling process func SetSubreaper(i int) error { return unix.Prctl(PR_SET_CHILD_SUBREAPER, uintptr(i), 0, 0, 0) diff --git a/libcontainer/system/unsupported.go b/libcontainer/system/unsupported.go index a8bea3022..b94be74a6 100644 --- a/libcontainer/system/unsupported.go +++ b/libcontainer/system/unsupported.go @@ -2,7 +2,11 @@ package system -import "github.com/opencontainers/runc/libcontainer/user" +import ( + "os" + + "github.com/opencontainers/runc/libcontainer/user" +) // RunningInUserNS is a stub for non-Linux systems // Always returns false @@ -15,3 +19,9 @@ func RunningInUserNS() bool { func UIDMapInUserNS(uidmap []user.IDMap) bool { return false } + +// GetParentNSeuid returns the euid within the parent user namespace +// Always returns os.Geteuid on non-linux +func GetParentNSeuid() int { + return os.Geteuid() +} diff --git a/main.go b/main.go index 1b9728c59..278399a56 100644 --- a/main.go +++ b/main.go @@ -63,7 +63,11 @@ func main() { app.Version = strings.Join(v, "\n") root := "/run/runc" - if os.Geteuid() != 0 { + rootless, err := isRootless(nil) + if err != nil { + fatal(err) + } + if rootless { runtimeDir := os.Getenv("XDG_RUNTIME_DIR") if runtimeDir != "" { root = runtimeDir + "/runc" @@ -108,6 +112,11 @@ func main() { Name: "systemd-cgroup", Usage: "enable systemd cgroup support, expects cgroupsPath to be of form \"slice:prefix:name\" for e.g. \"system.slice:runc:434234\"", }, + cli.StringFlag{ + Name: "rootless", + Value: "auto", + Usage: "enable rootless mode ('true', 'false', or 'auto')", + }, } app.Commands = []cli.Command{ checkpointCommand, diff --git a/ps.go b/ps.go index 6e0c7376a..eec9d5f56 100644 --- a/ps.go +++ b/ps.go @@ -29,7 +29,11 @@ var psCommand = cli.Command{ return err } // XXX: Currently not supported with rootless containers. - if isRootless() { + rootless, err := isRootless(context) + if err != nil { + return err + } + if rootless { return fmt.Errorf("runc ps requires root") } diff --git a/restore.go b/restore.go index 362be62de..724157da2 100644 --- a/restore.go +++ b/restore.go @@ -96,7 +96,11 @@ using the runc checkpoint command.`, return err } // XXX: Currently this is untested with rootless containers. - if isRootless() { + rootless, err := isRootless(context) + if err != nil { + return err + } + if rootless { return fmt.Errorf("runc restore requires root") } diff --git a/utils.go b/utils.go index 8ed1a88ea..5165336fb 100644 --- a/utils.go +++ b/utils.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + "strconv" + "strings" "github.com/opencontainers/runtime-spec/specs-go" @@ -81,3 +83,12 @@ func revisePidFile(context *cli.Context) error { } return context.Set("pid-file", pidFile) } + +// parseBoolOrAuto returns (nil, nil) if s is empty or "auto" +func parseBoolOrAuto(s string) (*bool, error) { + if s == "" || strings.ToLower(s) == "auto" { + return nil, nil + } + b, err := strconv.ParseBool(s) + return &b, err +} diff --git a/utils_linux.go b/utils_linux.go index 84731c844..2b7ddd584 100644 --- a/utils_linux.go +++ b/utils_linux.go @@ -16,6 +16,7 @@ import ( "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/intelrdt" "github.com/opencontainers/runc/libcontainer/specconv" + "github.com/opencontainers/runc/libcontainer/system" "github.com/opencontainers/runc/libcontainer/utils" "github.com/opencontainers/runtime-spec/specs-go" @@ -217,19 +218,37 @@ func createPidFile(path string, process *libcontainer.Process) error { return os.Rename(tmpName, path) } -// XXX: Currently we autodetect rootless mode. -func isRootless() bool { - return os.Geteuid() != 0 +func isRootless(context *cli.Context) (bool, error) { + if context != nil { + b, err := parseBoolOrAuto(context.GlobalString("rootless")) + if err != nil { + return false, err + } + if b != nil { + return *b, nil + } + // nil b stands for "auto detect" + } + // Even if os.Geteuid() == 0, it might still require rootless mode, + // especially when running within userns. + // So we use system.GetParentNSeuid() here. + // + // TODO(AkihiroSuda): how to support nested userns? + return system.GetParentNSeuid() != 0, nil } func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) { + rootless, err := isRootless(context) + if err != nil { + return nil, err + } config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{ CgroupName: id, UseSystemdCgroup: context.GlobalBool("systemd-cgroup"), NoPivotRoot: context.Bool("no-pivot"), NoNewKeyring: context.Bool("no-new-keyring"), Spec: spec, - Rootless: isRootless(), + Rootless: rootless, }) if err != nil { return nil, err From cdb7f23d21fa49c8ff1edfe3bfbd94d1550f550b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 May 2018 12:17:37 +0900 Subject: [PATCH 3/4] main: add condition to isRootless() Signed-off-by: Akihiro Suda --- utils_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils_linux.go b/utils_linux.go index 2b7ddd584..719ede638 100644 --- a/utils_linux.go +++ b/utils_linux.go @@ -234,7 +234,7 @@ func isRootless(context *cli.Context) (bool, error) { // So we use system.GetParentNSeuid() here. // // TODO(AkihiroSuda): how to support nested userns? - return system.GetParentNSeuid() != 0, nil + return system.GetParentNSeuid() != 0 || system.RunningInUserNS(), nil } func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) { From c93815738a53acae4998c5c9bd84dae18f864b42 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 22 May 2018 15:56:01 +0900 Subject: [PATCH 4/4] libcontainer: remove extra CAP_SETGID check for SetgroupAttr Signed-off-by: Akihiro Suda --- libcontainer/container_linux.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index 246ec95f3..34c2576dc 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -28,7 +28,6 @@ import ( "github.com/golang/protobuf/proto" "github.com/sirupsen/logrus" - "github.com/syndtr/gocapability/capability" "github.com/vishvananda/netlink/nl" "golang.org/x/sys/unix" ) @@ -1798,17 +1797,10 @@ func (c *linuxContainer) bootstrapData(cloneFlags uintptr, nsMaps map[configs.Na }) } if requiresRootOrMappingTool(c.config) { - // check if we have CAP_SETGID to setgroup properly - pid, err := capability.NewPid(0) - if err != nil { - return nil, err - } - if !pid.Get(capability.EFFECTIVE, capability.CAP_SETGID) { - r.AddData(&Boolmsg{ - Type: SetgroupAttr, - Value: true, - }) - } + r.AddData(&Boolmsg{ + Type: SetgroupAttr, + Value: true, + }) } } }