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/container_linux.go b/libcontainer/container_linux.go index 546a007c0..3160f9699 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, + }) } } } diff --git a/libcontainer/system/linux.go b/libcontainer/system/linux.go index 5f124cd8b..8d353d984 100644 --- a/libcontainer/system/linux.go +++ b/libcontainer/system/linux.go @@ -3,13 +3,12 @@ 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,34 +101,43 @@ 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 } +// 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/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..b94be74a6 100644 --- a/libcontainer/system/unsupported.go +++ b/libcontainer/system/unsupported.go @@ -2,8 +2,26 @@ package system +import ( + "os" + + "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 +} + +// 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/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 +} 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 c6223f0d0..66fb97c9b 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" @@ -220,19 +221,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 || system.RunningInUserNS(), 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