diff --git a/Makefile b/Makefile index 89e277881..f7315de70 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ LDFLAGS := -ldflags '-X main.gitCommit=${GIT_COMMIT} -X main.buildInfo=${BUILD_I all: buildah imgtype docs -buildah: *.go imagebuildah/*.go bind/*.go cmd/buildah/*.go docker/*.go pkg/cli/*.go pkg/parse/*.go util/*.go +buildah: *.go imagebuildah/*.go bind/*.go cmd/buildah/*.go docker/*.go pkg/cli/*.go pkg/parse/*.go unshare/*.c unshare/*.go util/*.go $(GO) build $(LDFLAGS) -o buildah $(BUILDFLAGS) ./cmd/buildah imgtype: *.go docker/*.go util/*.go tests/imgtype/imgtype.go diff --git a/cmd/buildah/main.go b/cmd/buildah/main.go index 7363a3ea0..6bb18fe4e 100644 --- a/cmd/buildah/main.go +++ b/cmd/buildah/main.go @@ -81,6 +81,7 @@ func main() { debug = true logrus.SetLevel(logrus.DebugLevel) } + maybeReexecUsingUserNamespace(c, false) return nil } app.After = func(c *cli.Context) error { @@ -110,6 +111,7 @@ func main() { runCommand, tagCommand, umountCommand, + unshareCommand, versionCommand, } err := app.Run(os.Args) diff --git a/cmd/buildah/unshare.go b/cmd/buildah/unshare.go new file mode 100644 index 000000000..567ef09e6 --- /dev/null +++ b/cmd/buildah/unshare.go @@ -0,0 +1,234 @@ +// +build linux + +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "os/user" + "runtime" + "strconv" + "syscall" + + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/projectatomic/buildah/unshare" + "github.com/projectatomic/buildah/util" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +const ( + // startedInUserNS is an environment variable that, if set, means that we shouldn't try + // to create and enter a new user namespace and then re-exec ourselves. + startedInUserNS = "_BUILDAH_STARTED_IN_USERNS" +) + +var ( + unshareDescription = "Runs a command in a modified user namespace" + unshareCommand = cli.Command{ + Name: "unshare", + Usage: "Run a command in a modified user namespace", + Description: unshareDescription, + Action: unshareCmd, + ArgsUsage: "[COMMAND [ARGS [...]]]", + SkipArgReorder: true, + } +) + +type runnable interface { + Run() error +} + +func maybeReexecUsingUserNamespace(c *cli.Context, evenForRoot bool) { + // If we've already been through this once, no need to try again. + if os.Getenv(startedInUserNS) != "" { + return + } + + // Figure out if we're already root, or "root", which is close enough, + // unless we've been explicitly told to do this even for root. + me, err := user.Current() + if err != nil { + logrus.Errorf("error determining current user: %v", err) + cli.OsExiter(1) + } + if me.Uid == "0" && !evenForRoot { + return + } + uidNum, err := strconv.ParseUint(me.Uid, 10, 32) + if err != nil { + logrus.Errorf("error parsing current UID %s: %v", me.Uid, err) + cli.OsExiter(1) + } + gidNum, err := strconv.ParseUint(me.Gid, 10, 32) + if err != nil { + logrus.Errorf("error parsing current GID %s: %v", me.Gid, err) + cli.OsExiter(1) + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Read the set of ID mappings that we're allowed to use. Each range + // in /etc/subuid and /etc/subgid file is a starting ID and a range size. + uidmap, gidmap, err := util.GetSubIDMappings(me.Username, me.Username) + if err != nil { + logrus.Errorf("error reading allowed ID mappings: %v", err) + cli.OsExiter(1) + } + if len(uidmap) == 0 { + logrus.Warnf("Found no UID ranges set aside for user %q in /etc/subuid.", me.Username) + } + if len(gidmap) == 0 { + logrus.Warnf("Found no GID ranges set aside for user %q in /etc/subgid.", me.Username) + } + + // Build modified maps that map us to uid/gid 0, and maps every other + // range to itself. In a namespace that uses this map, the invoking + // user will appear to be root. This should let us create storage + // directories and access credentials under the invoking user's home + // directory. + uidmap2 := append([]specs.LinuxIDMapping{{HostID: uint32(uidNum), ContainerID: 0, Size: 1}}, uidmap...) + for i := range uidmap2[1:] { + uidmap2[i+1].ContainerID = uidmap2[i+1].HostID + } + gidmap2 := append([]specs.LinuxIDMapping{{HostID: uint32(gidNum), ContainerID: 0, Size: 1}}, gidmap...) + for i := range gidmap2[1:] { + gidmap2[i+1].ContainerID = gidmap2[i+1].HostID + } + + // Map the uidmap and gidmap ranges, consecutively, starting at 0. + // When used to created a namespace inside of a namespace that uses the + // maps we've created above, they'll produce mappings which don't map + // in the invoking user. This is more suitable for running commands in + // containers, so we'll want to use it as a default for any containers + // that we create. + umap := new(bytes.Buffer) + for i := range uidmap { + if i > 0 { + fmt.Fprintf(umap, ",") + } + fmt.Fprintf(umap, "%d:%d:%d", uidmap[i].ContainerID, uidmap[i].HostID, uidmap[i].Size) + } + gmap := new(bytes.Buffer) + for i := range gidmap { + if i > 0 { + fmt.Fprintf(gmap, ",") + } + fmt.Fprintf(gmap, "%d:%d:%d", gidmap[i].ContainerID, gidmap[i].HostID, gidmap[i].Size) + } + + // Add args to change the global defaults. + defaultStorageDriver := "vfs" + defaultRoot, err := util.UnsharedRootPath(me.HomeDir) + if err != nil { + logrus.Errorf("%v", err) + cli.OsExiter(1) + } + defaultRunroot, err := util.UnsharedRunrootPath(me.Uid) + if err != nil { + logrus.Errorf("%v", err) + cli.OsExiter(1) + } + var moreArgs []string + if !c.GlobalIsSet("storage-driver") || !c.GlobalIsSet("root") || !c.GlobalIsSet("runroot") || (!c.GlobalIsSet("userns-uid-map") && !c.GlobalIsSet("userns-gid-map")) { + logrus.Infof("Running without privileges, assuming arguments:") + if !c.GlobalIsSet("storage-driver") { + logrus.Infof(" --storage-driver %q", defaultStorageDriver) + moreArgs = append(moreArgs, "--storage-driver", defaultStorageDriver) + } + if !c.GlobalIsSet("root") { + logrus.Infof(" --root %q", defaultRoot) + moreArgs = append(moreArgs, "--root", defaultRoot) + } + if !c.GlobalIsSet("runroot") { + logrus.Infof(" --runroot %q", defaultRunroot) + moreArgs = append(moreArgs, "--runroot", defaultRunroot) + } + if !c.GlobalIsSet("userns-uid-map") && !c.GlobalIsSet("userns-gid-map") && umap.Len() > 0 && gmap.Len() > 0 { + logrus.Infof(" --userns-uid-map %q --userns-gid-map %q", umap.String(), gmap.String()) + moreArgs = append(moreArgs, "--userns-uid-map", umap.String(), "--userns-gid-map", gmap.String()) + } + } + + // Unlike most uses of reexec or unshare, we're using a name that + // _won't_ be recognized as a registered reexec handler, since we + // _want_ to fall through reexec.Init() to the normal main(). + cmd := unshare.Command(append(append([]string{"buildah-unprivileged"}, moreArgs...), os.Args[1:]...)...) + + // If, somehow, we don't become UID 0 in our child, indicate that the child shouldn't try again. + if err = os.Setenv(startedInUserNS, "1"); err != nil { + logrus.Errorf("error setting %s=1 in environment: %v", startedInUserNS, err) + os.Exit(1) + } + + // Reuse our stdio. + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Set up a new user namespace with the ID mapping. + cmd.UnshareFlags = syscall.CLONE_NEWUSER + cmd.UseNewuidmap = true + cmd.UidMappings = uidmap2 + cmd.UseNewgidmap = true + cmd.GidMappings = gidmap2 + cmd.GidMappingsEnableSetgroups = true + + // Finish up. + logrus.Debugf("running %+v with environment %+v, UID map %+v, and GID map %+v", cmd.Cmd.Args, os.Environ(), cmd.UidMappings, cmd.GidMappings) + execRunnable(cmd) +} + +// execRunnable runs the specified unshare command, captures its exit status, +// and exits with the same status. +func execRunnable(cmd runnable) { + if err := cmd.Run(); err != nil { + if exitError, ok := errors.Cause(err).(*exec.ExitError); ok { + if exitError.ProcessState.Exited() { + if waitStatus, ok := exitError.ProcessState.Sys().(syscall.WaitStatus); ok { + if waitStatus.Exited() { + logrus.Errorf("%v", exitError) + os.Exit(waitStatus.ExitStatus()) + } + if waitStatus.Signaled() { + logrus.Errorf("%v", exitError) + os.Exit(int(waitStatus.Signal()) + 128) + } + } + } + } + logrus.Errorf("%v", err) + logrus.Errorf("(unable to determine exit status)") + os.Exit(1) + } + os.Exit(0) +} + +// unshareCmd execs whatever using the ID mappings that we want to use for ourselves +func unshareCmd(c *cli.Context) error { + // force reexec using the configured ID mappings + maybeReexecUsingUserNamespace(c, true) + // exec the specified command, if there is one + args := c.Args() + if len(args) < 1 { + // try to exec the shell, if one's set + shell, shellSet := os.LookupEnv("SHELL") + if !shellSet { + logrus.Errorf("no command specified") + os.Exit(1) + } + args = []string{shell} + } + cmd := exec.Command(args[0], args[1:]...) + cmd.Env = append(os.Environ(), "USER=root", "USERNAME=root", "GROUP=root", "LOGNAME=root", "UID=0", "GID=0") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + execRunnable(cmd) + os.Exit(1) + return nil +} diff --git a/cmd/buildah/unshare_unsupported.go b/cmd/buildah/unshare_unsupported.go new file mode 100644 index 000000000..e1359de32 --- /dev/null +++ b/cmd/buildah/unshare_unsupported.go @@ -0,0 +1,16 @@ +// +build !linux + +package main + +var ( + unshareCommand = cli.Command{ + Name: "unshare", + Hidden: true, + Action: func(c *cli.Context) error { return nil }, + SkipArgReorder: true, + } +) + +func maybeReexecUsingUserNamespace(c *cli.Context, evenForRoot bool) { + return +} diff --git a/contrib/completions/bash/buildah b/contrib/completions/bash/buildah index 9a1b6cb49..54e01506a 100644 --- a/contrib/completions/bash/buildah +++ b/contrib/completions/bash/buildah @@ -753,6 +753,16 @@ return 1 esac } + _buildah_unshare() { + local boolean_options=" + --help + -h + " + + local options_with_args=" + " + } + _buildah_version() { local boolean_options=" --help @@ -761,7 +771,7 @@ return 1 local options_with_args=" " -} + } _buildah() { local previous_extglob_setting=$(shopt -p extglob) @@ -787,6 +797,7 @@ return 1 tag umount unmount + unshare version ) diff --git a/contrib/rpm/buildah.spec b/contrib/rpm/buildah.spec index 60b39b356..7dfd0a541 100644 --- a/contrib/rpm/buildah.spec +++ b/contrib/rpm/buildah.spec @@ -27,7 +27,7 @@ Name: buildah # Bump version in buildah.go too Version: 1.2 -Release: 1.git%{shortcommit}%{?dist} +Release: 2.git%{shortcommit}%{?dist} Summary: A command line tool used to creating OCI Images License: ASL 2.0 URL: https://%{provider_prefix} @@ -49,6 +49,7 @@ BuildRequires: make Requires: runc >= 1.0.0-6 Requires: container-selinux Requires: skopeo-containers +Requires: shadow-utils Provides: %{repo} = %{version}-%{release} %description diff --git a/docs/buildah-unshare.md b/docs/buildah-unshare.md new file mode 100644 index 000000000..b4d162322 --- /dev/null +++ b/docs/buildah-unshare.md @@ -0,0 +1,32 @@ +# buildah-unshare "19" "June 2018" "buildah" + +## NAME +buildah\-unshare - Run a command inside of a modified user namespace. + +## SYNOPSIS +**buildah** **unshare** [*options* [...] --] [**command**] + +## DESCRIPTION +Launches a process (by default, *$SHELL*) in a new user namespace. The user +namespace is configured so that the invoking user's UID and primary GID appear +to be UID 0 and GID 0, respectively. Any ranges which match that user and +group in /etc/subuid and /etc/subgid are also mapped in as themselves with the +help of the *newuidmap(1)* and *newgidmap(1)* helpers. + +This is mainly useful for troubleshooting unprivileged operations and for +manually clearing storage and other data related to images and containers. + +## EXAMPLE + +buildah unshare id + +buildah unshare pwd + +buildah unshare cat /proc/self/uid\_map + +buildah unshare cat /proc/self/gid\_map + +buildah unshare rm -fr $HOME/.local/share/containers/storage /var/run/user/\`id -u\`/run + +## SEE ALSO +buildah(1), namespaces(7), newuidmap(1), newgidmap(1), user\_namespaces(7) diff --git a/docs/buildah.md b/docs/buildah.md index 26b0784ab..432b9474e 100644 --- a/docs/buildah.md +++ b/docs/buildah.md @@ -46,23 +46,23 @@ be used, as the default behavior of using the system-wide configuration **--root** **value** -Storage root dir (default: "/var/lib/containers/storage") +Storage root dir (default: "/var/lib/containers/storage" for UID 0, "$HOME/.local/share/containers/storage" for other users) Default root dir is configured in /etc/containers/storage.conf **--runroot** **value** -Storage state dir (default: "/var/run/containers/storage") +Storage state dir (default: "/var/run/containers/storage" for UID 0, "/var/run/user/$UID/run" for other users) Default state dir is configured in /etc/containers/storage.conf **--storage-driver** **value** -Storage driver. Default Storage driver is configured in /etc/containers/storage.conf. Overriding -this option, will drop the storage-opt definitions was well from the storage.conf file. User must +Storage driver. The default storage driver for UID 0 is configured in /etc/containers/storage.conf, and is *vfs* for other users. +Overriding this option will cause the *storage-opt* settings in /etc/containers/storage.conf to be ignored. The user must specify additional options via the `--storage-opt` flag. **--storage-opt** **value** -Storage driver option, Default Storage driver options are configured in /etc/containers/storage.conf +Storage driver option, Default storage driver options are configured in /etc/containers/storage.conf **--userns-uid-map** *mapping* @@ -107,8 +107,9 @@ Print the version | buildah-run(1) | Run a command inside of the container. | | buildah-tag(1) | Add an additional name to a local image. | | buildah-umount(1) | Unmount a working container's root file system. | -| buildah-version(1) | Display the Buildah Version Information | -| storage.conf(5) | Syntax of Container Storage configuration file | +| buildah-unshare(1) | Launch a command in a user namespace with modified ID mappings. | +| buildah-version(1) | Display the Buildah Version Information | +| storage.conf(5) | Syntax of Container Storage configuration file | ## Files @@ -134,7 +135,7 @@ Print the version registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. ## SEE ALSO -podman(1), mounts.conf(5), registries.conf(5), storage.conf(5) +podman(1), mounts.conf(5), newuidmap(1), newgidmap(1), registries.conf(5), storage.conf(5) ## HISTORY December 2017, Originally compiled by Tom Sweeney diff --git a/unshare/unshare.c b/unshare/unshare.c new file mode 100644 index 000000000..83864359b --- /dev/null +++ b/unshare/unshare.c @@ -0,0 +1,110 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int _buildah_unshare_parse_envint(const char *envname) { + char *p, *q; + long l; + + p = getenv(envname); + if (p == NULL) { + return -1; + } + q = NULL; + l = strtol(p, &q, 10); + if ((q == NULL) || (*q != '\0')) { + fprintf(stderr, "Error parsing \"%s\"=\"%s\"!\n", envname, p); + _exit(1); + } + unsetenv(envname); + return l; +} + +void _buildah_unshare(void) +{ + int flags, pidfd, continuefd, n, pgrp, sid, ctty, allow_setgroups; + char buf[2048]; + + flags = _buildah_unshare_parse_envint("_Buildah-unshare"); + if (flags == -1) { + return; + } + if ((flags & CLONE_NEWUSER) != 0) { + if (unshare(CLONE_NEWUSER) == -1) { + fprintf(stderr, "Error during unshare(CLONE_NEWUSER): %m\n"); + _exit(1); + } + } + pidfd = _buildah_unshare_parse_envint("_Buildah-pid-pipe"); + if (pidfd != -1) { + snprintf(buf, sizeof(buf), "%llu", (unsigned long long) getpid()); + if (write(pidfd, buf, strlen(buf)) != strlen(buf)) { + fprintf(stderr, "Error writing PID to pipe on fd %d: %m\n", pidfd); + _exit(1); + } + close(pidfd); + } + continuefd = _buildah_unshare_parse_envint("_Buildah-continue-pipe"); + if (continuefd != -1) { + n = read(continuefd, buf, sizeof(buf)); + if (n > 0) { + fprintf(stderr, "Error: %.*s\n", n, buf); + _exit(1); + } + close(continuefd); + } + sid = _buildah_unshare_parse_envint("_Buildah-setsid"); + if (sid == 1) { + if (setsid() == -1) { + fprintf(stderr, "Error during setsid: %m\n"); + _exit(1); + } + } + pgrp = _buildah_unshare_parse_envint("_Buildah-setpgrp"); + if (pgrp == 1) { + if (setpgrp() == -1) { + fprintf(stderr, "Error during setpgrp: %m\n"); + _exit(1); + } + } + ctty = _buildah_unshare_parse_envint("_Buildah-ctty"); + if (ctty != -1) { + if (ioctl(ctty, TIOCSCTTY, 0) == -1) { + fprintf(stderr, "Error while setting controlling terminal to %d: %m\n", ctty); + _exit(1); + } + } + allow_setgroups = _buildah_unshare_parse_envint("_Buildah-allow-setgroups"); + if ((flags & CLONE_NEWUSER) != 0) { + if (allow_setgroups == 1) { + if (setgroups(0, NULL) != 0) { + fprintf(stderr, "Error during setgroups(0, NULL): %m\n"); + _exit(1); + } + } + if (setresgid(0, 0, 0) != 0) { + fprintf(stderr, "Error during setresgid(0): %m\n"); + _exit(1); + } + if (setresuid(0, 0, 0) != 0) { + fprintf(stderr, "Error during setresuid(0): %m\n"); + _exit(1); + } + } + if ((flags & ~CLONE_NEWUSER) != 0) { + if (unshare(flags & ~CLONE_NEWUSER) == -1) { + fprintf(stderr, "Error during unshare(...): %m\n"); + _exit(1); + } + } + return; +} diff --git a/unshare/unshare.go b/unshare/unshare.go new file mode 100644 index 000000000..56b4356e2 --- /dev/null +++ b/unshare/unshare.go @@ -0,0 +1,273 @@ +// +build linux + +package unshare + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "syscall" + + "github.com/containers/storage/pkg/reexec" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/projectatomic/buildah/util" +) + +// Cmd wraps an exec.Cmd created by the reexec package in unshare(), and +// handles setting ID maps and other related settings by triggering +// initialization code in the child. +type Cmd struct { + *exec.Cmd + UnshareFlags int + UseNewuidmap bool + UidMappings []specs.LinuxIDMapping + UseNewgidmap bool + GidMappings []specs.LinuxIDMapping + GidMappingsEnableSetgroups bool + Setsid bool + Setpgrp bool + Ctty *os.File + OOMScoreAdj int + Hook func(pid int) error +} + +// Command creates a new Cmd which can be customized. +func Command(args ...string) *Cmd { + cmd := reexec.Command(args...) + return &Cmd{ + Cmd: cmd, + } +} + +func (c *Cmd) Start() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Set an environment variable to tell the child to synchronize its startup. + if c.Env == nil { + c.Env = os.Environ() + } + c.Env = append(c.Env, fmt.Sprintf("_Buildah-unshare=%d", c.UnshareFlags)) + + // Create the pipe for reading the child's PID. + pidRead, pidWrite, err := os.Pipe() + if err != nil { + return errors.Wrapf(err, "error creating pid pipe") + } + c.Env = append(c.Env, fmt.Sprintf("_Buildah-pid-pipe=%d", len(c.ExtraFiles)+3)) + c.ExtraFiles = append(c.ExtraFiles, pidWrite) + + // Create the pipe for letting the child know to proceed. + continueRead, continueWrite, err := os.Pipe() + if err != nil { + pidRead.Close() + pidWrite.Close() + return errors.Wrapf(err, "error creating pid pipe") + } + c.Env = append(c.Env, fmt.Sprintf("_Buildah-continue-pipe=%d", len(c.ExtraFiles)+3)) + c.ExtraFiles = append(c.ExtraFiles, continueRead) + + // Pass along other instructions. + if c.Setsid { + c.Env = append(c.Env, "_Buildah-setsid=1") + } + if c.Setpgrp { + c.Env = append(c.Env, "_Buildah-setpgrp=1") + } + if c.Ctty != nil { + c.Env = append(c.Env, fmt.Sprintf("_Buildah-ctty=%d", len(c.ExtraFiles)+3)) + c.ExtraFiles = append(c.ExtraFiles, c.Ctty) + } + if c.GidMappingsEnableSetgroups { + c.Env = append(c.Env, "_Buildah-allow-setgroups=1") + } else { + c.Env = append(c.Env, "_Buildah-allow-setgroups=0") + } + + // Make sure we clean up our pipes. + defer func() { + if pidRead != nil { + pidRead.Close() + } + if pidWrite != nil { + pidWrite.Close() + } + if continueRead != nil { + continueRead.Close() + } + if continueWrite != nil { + continueWrite.Close() + } + }() + + // Start the new process. + err = c.Cmd.Start() + if err != nil { + return err + } + + // Close the ends of the pipes that the parent doesn't need. + continueRead.Close() + continueRead = nil + pidWrite.Close() + pidWrite = nil + + // Read the child's PID from the pipe. + pidString := "" + b := new(bytes.Buffer) + io.Copy(b, pidRead) + pidString = b.String() + pid, err := strconv.Atoi(pidString) + if err != nil { + fmt.Fprintf(continueWrite, "error parsing PID %q: %v", pidString, err) + return errors.Wrapf(err, "error parsing PID %q", pidString) + } + pidString = fmt.Sprintf("%d", pid) + + // If we created a new user namespace, set any specified mappings. + if c.UnshareFlags&syscall.CLONE_NEWUSER != 0 { + // Always set "setgroups". + setgroups, err := os.OpenFile(fmt.Sprintf("/proc/%s/setgroups", pidString), os.O_TRUNC|os.O_WRONLY, 0) + if err != nil { + fmt.Fprintf(continueWrite, "error opening setgroups: %v", err) + return errors.Wrapf(err, "error opening /proc/%s/setgroups", pidString) + } + defer setgroups.Close() + if c.GidMappingsEnableSetgroups { + if _, err := fmt.Fprintf(setgroups, "allow"); err != nil { + fmt.Fprintf(continueWrite, "error writing \"allow\" to setgroups: %v", err) + return errors.Wrapf(err, "error opening \"allow\" to /proc/%s/setgroups", pidString) + } + } else { + if _, err := fmt.Fprintf(setgroups, "deny"); err != nil { + fmt.Fprintf(continueWrite, "error writing \"deny\" to setgroups: %v", err) + return errors.Wrapf(err, "error writing \"deny\" to /proc/%s/setgroups", pidString) + } + } + + if len(c.UidMappings) == 0 || len(c.GidMappings) == 0 { + uidmap, gidmap, err := util.GetHostIDMappings("") + if err != nil { + fmt.Fprintf(continueWrite, "error reading ID mappings in parent: %v", err) + return errors.Wrapf(err, "error reading ID mappings in parent") + } + if len(c.UidMappings) == 0 { + c.UidMappings = uidmap + for i := range c.UidMappings { + c.UidMappings[i].HostID = c.UidMappings[i].ContainerID + } + } + if len(c.GidMappings) == 0 { + c.GidMappings = gidmap + for i := range c.GidMappings { + c.GidMappings[i].HostID = c.GidMappings[i].ContainerID + } + } + } + + if len(c.GidMappings) > 0 { + // Build the GID map, since writing to the proc file has to be done all at once. + g := new(bytes.Buffer) + for _, m := range c.GidMappings { + fmt.Fprintf(g, "%d %d %d\n", m.ContainerID, m.HostID, m.Size) + } + // Set the GID map. + if c.UseNewgidmap { + cmd := exec.Command("newgidmap", append([]string{pidString}, strings.Fields(strings.Replace(g.String(), "\n", " ", -1))...)...) + g.Reset() + cmd.Stdout = g + cmd.Stderr = g + err := cmd.Run() + if err != nil { + fmt.Fprintf(continueWrite, "error running newgidmap: %v: %s", err, g.String()) + return errors.Wrapf(err, "error running newgidmap: %s", g.String()) + } + } else { + gidmap, err := os.OpenFile(fmt.Sprintf("/proc/%s/gid_map", pidString), os.O_TRUNC|os.O_WRONLY, 0) + if err != nil { + fmt.Fprintf(continueWrite, "error opening /proc/%s/gid_map: %v", pidString, err) + return errors.Wrapf(err, "error opening /proc/%s/gid_map", pidString) + } + defer gidmap.Close() + if _, err := fmt.Fprintf(gidmap, "%s", g.String()); err != nil { + fmt.Fprintf(continueWrite, "error writing /proc/%s/gid_map: %v", pidString, err) + return errors.Wrapf(err, "error writing /proc/%s/gid_map", pidString) + } + } + } + + if len(c.UidMappings) > 0 { + // Build the UID map, since writing to the proc file has to be done all at once. + u := new(bytes.Buffer) + for _, m := range c.UidMappings { + fmt.Fprintf(u, "%d %d %d\n", m.ContainerID, m.HostID, m.Size) + } + // Set the GID map. + if c.UseNewuidmap { + cmd := exec.Command("newuidmap", append([]string{pidString}, strings.Fields(strings.Replace(u.String(), "\n", " ", -1))...)...) + u.Reset() + cmd.Stdout = u + cmd.Stderr = u + err := cmd.Run() + if err != nil { + fmt.Fprintf(continueWrite, "error running newuidmap: %v: %s", err, u.String()) + return errors.Wrapf(err, "error running newuidmap: %s", u.String()) + } + } else { + uidmap, err := os.OpenFile(fmt.Sprintf("/proc/%s/uid_map", pidString), os.O_TRUNC|os.O_WRONLY, 0) + if err != nil { + fmt.Fprintf(continueWrite, "error opening /proc/%s/uid_map: %v", pidString, err) + return errors.Wrapf(err, "error opening /proc/%s/uid_map", pidString) + } + defer uidmap.Close() + if _, err := fmt.Fprintf(uidmap, "%s", u.String()); err != nil { + fmt.Fprintf(continueWrite, "error writing /proc/%s/uid_map: %v", pidString, err) + return errors.Wrapf(err, "error writing /proc/%s/uid_map", pidString) + } + } + } + } + + // Adjust the process's OOM score. + oomScoreAdj, err := os.OpenFile(fmt.Sprintf("/proc/%s/oom_score_adj", pidString), os.O_TRUNC|os.O_WRONLY, 0) + if err != nil { + fmt.Fprintf(continueWrite, "error opening oom_score_adj: %v", err) + return errors.Wrapf(err, "error opening /proc/%s/oom_score_adj", pidString) + } + if _, err := fmt.Fprintf(oomScoreAdj, "%d\n", c.OOMScoreAdj); err != nil { + fmt.Fprintf(continueWrite, "error writing \"%d\" to oom_score_adj: %v", c.OOMScoreAdj, err) + return errors.Wrapf(err, "error writing \"%d\" to /proc/%s/oom_score_adj", c.OOMScoreAdj) + } + defer oomScoreAdj.Close() + + // Run any additional setup that we want to do before the child starts running proper. + if c.Hook != nil { + if err = c.Hook(pid); err != nil { + fmt.Fprintf(continueWrite, "hook error: %v", err) + return err + } + } + + return nil +} + +func (c *Cmd) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +func (c *Cmd) CombinedOutput() ([]byte, error) { + return nil, errors.New("unshare: CombinedOutput() not implemented") +} + +func (c *Cmd) Output() ([]byte, error) { + return nil, errors.New("unshare: Output() not implemented") +} diff --git a/unshare/unshare_cgo.go b/unshare/unshare_cgo.go new file mode 100644 index 000000000..26a0b2c20 --- /dev/null +++ b/unshare/unshare_cgo.go @@ -0,0 +1,10 @@ +// +build linux,cgo,!gccgo + +package unshare + +// #cgo CFLAGS: -Wall +// extern void _buildah_unshare(void); +// void __attribute__((constructor)) init(void) { +// _buildah_unshare(); +// } +import "C" diff --git a/unshare/unshare_gccgo.go b/unshare/unshare_gccgo.go new file mode 100644 index 000000000..c4811782a --- /dev/null +++ b/unshare/unshare_gccgo.go @@ -0,0 +1,25 @@ +// +build linux,cgo,gccgo + +package unshare + +// #cgo CFLAGS: -Wall -Wextra +// extern void _buildah_unshare(void); +// void __attribute__((constructor)) init(void) { +// _buildah_unshare(); +// } +import "C" + +// This next bit is straight out of libcontainer. + +// AlwaysFalse is here to stay false +// (and be exported so the compiler doesn't optimize out its reference) +var AlwaysFalse bool + +func init() { + if AlwaysFalse { + // by referencing this C init() in a noop test, it will ensure the compiler + // links in the C function. + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65134 + C.init() + } +} diff --git a/unshare/unshare_test.go b/unshare/unshare_test.go new file mode 100644 index 000000000..500f6cf67 --- /dev/null +++ b/unshare/unshare_test.go @@ -0,0 +1,255 @@ +// +build linux + +package unshare + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "os" + "strconv" + "strings" + "syscall" + "testing" + + "github.com/containers/storage/pkg/reexec" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/projectatomic/buildah/util" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +func TestMain(m *testing.M) { + if reexec.Init() { + return + } + os.Exit(m.Run()) +} + +func init() { + reexec.Register("report", report) +} + +var ( + CloneFlags = map[string]int{ + "ipc": syscall.CLONE_NEWIPC, + "net": syscall.CLONE_NEWNET, + "mnt": syscall.CLONE_NEWNS, + "user": syscall.CLONE_NEWUSER, + "uts": syscall.CLONE_NEWUTS, + } +) + +type Report struct { + Namespaces map[string]string + UIDMappings []specs.LinuxIDMapping + GIDMappings []specs.LinuxIDMapping + Pgrp int + Sid int + OOMScoreAdj int +} + +func report() { + var report Report + report.Namespaces = make(map[string]string) + + for name := range CloneFlags { + linkTarget, err := os.Readlink("/proc/self/ns/" + name) + if err != nil { + logrus.Errorf("error reading link /proc/self/ns/%s: %v", name, err) + os.Exit(1) + } + report.Namespaces[name] = linkTarget + } + + report.Pgrp = syscall.Getpgrp() + + sid, err := unix.Getsid(unix.Getpid()) + if err != nil { + logrus.Errorf("error reading current session ID: %v", err) + os.Exit(1) + } + report.Sid = sid + + oomBytes, err := ioutil.ReadFile("/proc/self/oom_score_adj") + if err != nil { + logrus.Errorf("error reading current oom_score_adj: %v", err) + os.Exit(1) + } + oomFields := strings.Fields(string(oomBytes)) + if len(oomFields) != 1 { + logrus.Errorf("error parsing current oom_score_adj %q: wrong number of fields", string(oomBytes)) + os.Exit(1) + } + oom, err := strconv.Atoi(oomFields[0]) + if err != nil { + logrus.Errorf("error parsing current oom_score_adj %q: %v", oomFields[0], err) + os.Exit(1) + } + report.OOMScoreAdj = oom + + uidmap, gidmap, err := util.GetHostIDMappings("") + if err != nil { + logrus.Errorf("error reading current ID mappings: %v", err) + os.Exit(1) + } + for _, m := range uidmap { + report.UIDMappings = append(report.UIDMappings, m) + } + for _, m := range gidmap { + report.GIDMappings = append(report.GIDMappings, m) + } + + json.NewEncoder(os.Stdout).Encode(report) +} + +func TestUnshareNamespaces(t *testing.T) { + for name, flag := range CloneFlags { + var report Report + buf := new(bytes.Buffer) + cmd := Command("report") + cmd.UnshareFlags = syscall.CLONE_NEWUSER | flag + cmd.UidMappings = []specs.LinuxIDMapping{{HostID: uint32(syscall.Getuid()), ContainerID: 0, Size: 1}} + cmd.GidMappings = []specs.LinuxIDMapping{{HostID: uint32(syscall.Getgid()), ContainerID: 0, Size: 1}} + cmd.Stdout = buf + cmd.Stderr = buf + err := cmd.Run() + if err != nil { + t.Fatalf("run %q: %v: %s", name, err, buf.String()) + break + } + if err = json.Unmarshal(buf.Bytes(), &report); err != nil { + t.Fatalf("error parsing results: %v", err) + break + } + for ns := range CloneFlags { + linkTarget, err := os.Readlink("/proc/self/ns/" + ns) + if err != nil { + t.Fatalf("error reading link /proc/self/ns/%s: %v", ns, err) + os.Exit(1) + } + if ns == name || ns == "user" { // we always create a new user namespace + if report.Namespaces[ns] == linkTarget { + t.Fatalf("child is still in our %q namespace", name) + os.Exit(1) + } + } else { + if report.Namespaces[ns] != linkTarget { + t.Fatalf("child is not in our %q namespace", name) + os.Exit(1) + } + } + } + } +} + +func TestUnsharePgrp(t *testing.T) { + for _, same := range []bool{false, true} { + var report Report + buf := new(bytes.Buffer) + cmd := Command("report") + cmd.Setpgrp = !same + cmd.Stdout = buf + cmd.Stderr = buf + err := cmd.Run() + if err != nil { + t.Fatalf("run: %v: %s", err, buf.String()) + break + } + if err = json.Unmarshal(buf.Bytes(), &report); err != nil { + t.Fatalf("error parsing results: %v", err) + break + } + if (report.Pgrp == syscall.Getpgrp()) != same { + t.Fatalf("expected %d == %d to be %v", report.Pgrp, syscall.Getpgrp(), same) + } + } +} + +func TestUnshareSid(t *testing.T) { + sid, err := unix.Getsid(unix.Getpid()) + if err != nil { + t.Fatalf("error reading current session ID: %v", err) + } + for _, same := range []bool{false, true} { + var report Report + buf := new(bytes.Buffer) + cmd := Command("report") + cmd.Setsid = !same + cmd.Stdout = buf + cmd.Stderr = buf + err := cmd.Run() + if err != nil { + t.Fatalf("run: %v: %s", err, buf.String()) + break + } + if err = json.Unmarshal(buf.Bytes(), &report); err != nil { + t.Fatalf("error parsing results: %v", err) + break + } + if (report.Sid == sid) != same { + t.Fatalf("expected %d == %d to be %v", report.Sid, sid, same) + } + } +} + +func TestUnshareOOMScoreAdj(t *testing.T) { + for _, adj := range []int{0, 1, 2, 3} { + var report Report + buf := new(bytes.Buffer) + cmd := Command("report") + cmd.OOMScoreAdj = adj + cmd.Stdout = buf + cmd.Stderr = buf + err := cmd.Run() + if err != nil { + t.Fatalf("run: %v: %s", err, buf.String()) + break + } + if err = json.Unmarshal(buf.Bytes(), &report); err != nil { + t.Fatalf("error parsing results: %v", err) + break + } + if report.OOMScoreAdj != adj { + t.Fatalf("saw oom_score_adj %d to be %v", adj, report.OOMScoreAdj) + } + } +} + +func TestUnshareIDMappings(t *testing.T) { + var report Report + buf := new(bytes.Buffer) + cmd := Command("report") + cmd.UnshareFlags = syscall.CLONE_NEWUSER + cmd.UidMappings = []specs.LinuxIDMapping{{HostID: uint32(syscall.Getuid()), ContainerID: 0, Size: 1}} + cmd.GidMappings = []specs.LinuxIDMapping{{HostID: uint32(syscall.Getgid()), ContainerID: 0, Size: 1}} + cmd.Stdout = buf + cmd.Stderr = buf + err := cmd.Run() + if err != nil { + t.Fatalf("run: %v: %s", err, buf.String()) + } + if err = json.Unmarshal(buf.Bytes(), &report); err != nil { + t.Fatalf("error parsing results: %v", err) + } + if len(cmd.UidMappings) != len(report.UIDMappings) { + t.Fatalf("set %d UID mappings, read %d instead", len(cmd.UidMappings), len(report.UIDMappings)) + } + for i := range cmd.UidMappings { + if cmd.UidMappings[i].ContainerID != report.UIDMappings[i].ContainerID || + cmd.UidMappings[i].HostID != report.UIDMappings[i].HostID || + cmd.UidMappings[i].Size != report.UIDMappings[i].Size { + t.Fatalf("uid map entry %#v != %#v", cmd.UidMappings[i], report.UIDMappings[i]) + } + } + if len(cmd.GidMappings) != len(report.GIDMappings) { + t.Fatalf("set %d GID mappings, read %d instead", len(cmd.GidMappings), len(report.GIDMappings)) + } + for i := range cmd.GidMappings { + if cmd.GidMappings[i].ContainerID != report.GIDMappings[i].ContainerID || + cmd.GidMappings[i].HostID != report.GIDMappings[i].HostID || + cmd.GidMappings[i].Size != report.GIDMappings[i].Size { + t.Fatalf("gid map entry %#v != %#v", cmd.GidMappings[i], report.GIDMappings[i]) + } + } +} diff --git a/unshare/unshare_unsupported.go b/unshare/unshare_unsupported.go new file mode 100644 index 000000000..feeceae66 --- /dev/null +++ b/unshare/unshare_unsupported.go @@ -0,0 +1 @@ +package unshare diff --git a/util/util.go b/util/util.go index 61705867d..a97fdaf29 100644 --- a/util/util.go +++ b/util/util.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "path" + "path/filepath" "strconv" "strings" @@ -330,7 +331,8 @@ func getHostIDMappings(path string) ([]specs.LinuxIDMapping, error) { return mappings, nil } -// GetHostIDMappings reads mappings for the current process from the kernel. +// GetHostIDMappings reads mappings for the specified process (or the current +// process if pid is "self" or an empty string) from the kernel. func GetHostIDMappings(pid string) ([]specs.LinuxIDMapping, []specs.LinuxIDMapping, error) { if pid == "" { pid = "self" @@ -428,3 +430,37 @@ func ParseIDMappings(uidmap, gidmap []string) ([]idtools.IDMap, []idtools.IDMap, } return uid, gid, nil } + +// UnsharedRootPath returns a location under ($XDG_DATA_HOME/containers/storage, +// or $HOME/.local/share/containers/storage, or +// (the user's home directory)/.local/share/containers/storage, or an error. +func UnsharedRootPath(homedir string) (string, error) { + // If $XDG_DATA_HOME is defined... + if envDataHome, haveDataHome := os.LookupEnv("XDG_DATA_HOME"); haveDataHome { + return filepath.Join(envDataHome, "containers", "storage"), nil + } + // If $XDG_DATA_HOME is not defined, but $HOME is defined... + if envHomedir, haveHomedir := os.LookupEnv("HOME"); haveHomedir { + // Default to the user's $HOME/.local/share/containers/storage subdirectory. + return filepath.Join(envHomedir, ".local", "share", "containers", "storage"), nil + } + // If we know where our home directory is... + if homedir != "" { + // Default to the user's homedir/.local/share/containers/storage subdirectory. + return filepath.Join(homedir, ".local", "share", "containers", "storage"), nil + } + return "", errors.New("unable to determine a --root location: neither $XDG_DATA_HOME nor $HOME is set") +} + +// UnsharedRunrootPath returns $XDG_RUNTIME_DIR/run, /var/run/user/(the user's UID)/run, or an error. +func UnsharedRunrootPath(uid string) (string, error) { + // If $XDG_RUNTIME_DIR is defined... + if envRuntimeDir, haveRuntimeDir := os.LookupEnv("XDG_RUNTIME_DIR"); haveRuntimeDir { + return filepath.Join(envRuntimeDir, "run"), nil + } + // If $XDG_RUNTIME_DIR is not defined, but we know our UID... + if uid != "" { + return filepath.Join("/var/run/user", uid, "run"), nil + } + return "", errors.New("unable to determine a --runroot location: $XDG_RUNTIME_DIR is not set, and we don't know our UID") +}