package buildah import ( "bufio" "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" "syscall" "time" "github.com/containernetworking/cni/libcni" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/ioutils" "github.com/containers/storage/pkg/reexec" units "github.com/docker/go-units" digest "github.com/opencontainers/go-digest" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/projectatomic/buildah/bind" "github.com/projectatomic/buildah/util" "github.com/projectatomic/libpod/pkg/secrets" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh/terminal" "golang.org/x/sys/unix" ) const ( // DefaultWorkingDir is used if none was specified. DefaultWorkingDir = "/" // runUsingRuntimeCommand is a command we use as a key for reexec runUsingRuntimeCommand = Package + "-runtime" ) // TerminalPolicy takes the value DefaultTerminal, WithoutTerminal, or WithTerminal. type TerminalPolicy int const ( // DefaultTerminal indicates that this Run invocation should be // connected to a pseudoterminal if we're connected to a terminal. DefaultTerminal TerminalPolicy = iota // WithoutTerminal indicates that this Run invocation should NOT be // connected to a pseudoterminal. WithoutTerminal // WithTerminal indicates that this Run invocation should be connected // to a pseudoterminal. WithTerminal ) // String converts a TerminalPoliicy into a string. func (t TerminalPolicy) String() string { switch t { case DefaultTerminal: return "DefaultTerminal" case WithoutTerminal: return "WithoutTerminal" case WithTerminal: return "WithTerminal" } return fmt.Sprintf("unrecognized terminal setting %d", t) } // NamespaceOption controls how we set up a namespace when launching processes. type NamespaceOption struct { // Name specifies the type of namespace, typically matching one of the // ...Namespace constants defined in // github.com/opencontainers/runtime-spec/specs-go. Name string // Host is used to force our processes to use the host's namespace of // this type. Host bool // Path is the path of the namespace to attach our process to, if Host // is not set. If Host is not set and Path is also empty, a new // namespace will be created for the process that we're starting. // If Name is specs.NetworkNamespace, if Path doesn't look like an // absolute path, it is treated as a comma-separated list of CNI // configuration names which will be selected from among all of the CNI // network configurations which we find. Path string } // NamespaceOptions provides some helper methods for a slice of NamespaceOption // structs. type NamespaceOptions []NamespaceOption // IDMappingOptions controls how we set up UID/GID mapping when we set up a // user namespace. type IDMappingOptions struct { HostUIDMapping bool HostGIDMapping bool UIDMap []specs.LinuxIDMapping GIDMap []specs.LinuxIDMapping } // Isolation provides a way to specify whether we're supposed to use a proper // OCI runtime, or some other method for running commands. type Isolation int const ( // IsolationDefault is whatever we think will work best. IsolationDefault Isolation = iota // IsolationOCI is a proper OCI runtime. IsolationOCI ) // String converts a Isolation into a string. func (i Isolation) String() string { switch i { case IsolationDefault: return "IsolationDefault" case IsolationOCI: return "IsolationOCI" } return fmt.Sprintf("unrecognized isolation type %d", i) } // RunOptions can be used to alter how a command is run in the container. type RunOptions struct { // Hostname is the hostname we set for the running container. Hostname string // Isolation is either IsolationDefault or IsolationOCI. Isolation Isolation // Runtime is the name of the command to run. It should accept the same arguments // that runc does, and produce similar output. Runtime string // Args adds global arguments for the runtime. Args []string // Mounts are additional mount points which we want to provide. Mounts []specs.Mount // Env is additional environment variables to set. Env []string // User is the user as whom to run the command. User string // WorkingDir is an override for the working directory. WorkingDir string // Shell is default shell to run in a container. Shell string // Cmd is an override for the configured default command. Cmd []string // Entrypoint is an override for the configured entry point. Entrypoint []string // NamespaceOptions controls how we set up the namespaces for the process. NamespaceOptions NamespaceOptions // ConfigureNetwork controls whether or not network interfaces and // routing are configured for a new network namespace (i.e., when not // joining another's namespace and not just using the host's // namespace), effectively deciding whether or not the process has a // usable network. ConfigureNetwork NetworkConfigurationPolicy // CNIPluginPath is the location of CNI plugin helpers, if they should be // run from a location other than the default location. CNIPluginPath string // CNIConfigDir is the location of CNI configuration files, if the files in // the default configuration directory shouldn't be used. CNIConfigDir string // Terminal provides a way to specify whether or not the command should // be run with a pseudoterminal. By default (DefaultTerminal), a // terminal is used if os.Stdout is connected to a terminal, but that // decision can be overridden by specifying either WithTerminal or // WithoutTerminal. Terminal TerminalPolicy // TerminalSize provides a way to set the number of rows and columns in // a pseudo-terminal, if we create one, and Stdin/Stdout/Stderr aren't // connected to a terminal. TerminalSize *specs.Box // The stdin/stdout/stderr descriptors to use. If set to nil, the // corresponding files in the "os" package are used as defaults. Stdin io.Reader `json:"-"` Stdout io.Writer `json:"-"` Stderr io.Writer `json:"-"` // Quiet tells the run to turn off output to stdout. Quiet bool // AddCapabilities is a list of capabilities to add to the default set. AddCapabilities []string // DropCapabilities is a list of capabilities to remove from the default set, // after processing the AddCapabilities set. If a capability appears in both // lists, it will be dropped. DropCapabilities []string } // DefaultNamespaceOptions returns the default namespace settings from the // runtime-tools generator library. func DefaultNamespaceOptions() (NamespaceOptions, error) { options := NamespaceOptions{ {Name: string(specs.CgroupNamespace), Host: true}, {Name: string(specs.IPCNamespace), Host: true}, {Name: string(specs.MountNamespace), Host: true}, {Name: string(specs.NetworkNamespace), Host: true}, {Name: string(specs.PIDNamespace), Host: true}, {Name: string(specs.UserNamespace), Host: true}, {Name: string(specs.UTSNamespace), Host: true}, } g, err := generate.New("linux") if err != nil { return options, err } spec := g.Config if spec.Linux != nil { for _, ns := range spec.Linux.Namespaces { options.AddOrReplace(NamespaceOption{ Name: string(ns.Type), Path: ns.Path, }) } } return options, nil } // Find the configuration for the namespace of the given type. If there are // duplicates, find the _last_ one of the type, since we assume it was appended // more recently. func (n *NamespaceOptions) Find(namespace string) *NamespaceOption { for i := range *n { j := len(*n) - 1 - i if (*n)[j].Name == namespace { return &((*n)[j]) } } return nil } // AddOrReplace either adds or replaces the configuration for a given namespace. func (n *NamespaceOptions) AddOrReplace(options ...NamespaceOption) { nextOption: for _, option := range options { for i := range *n { j := len(*n) - 1 - i if (*n)[j].Name == option.Name { (*n)[j] = option continue nextOption } } *n = append(*n, option) } } func addRlimits(ulimit []string, g *generate.Generator) error { var ( ul *units.Ulimit err error ) for _, u := range ulimit { if ul, err = units.ParseUlimit(u); err != nil { return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) } g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Hard), uint64(ul.Soft)) } return nil } func addHosts(hosts []string, w io.Writer) error { buf := bufio.NewWriter(w) for _, host := range hosts { values := strings.SplitN(host, ":", 2) if len(values) != 2 { return errors.Errorf("unable to parse host entry %q: incorrect format", host) } if values[0] == "" { return errors.Errorf("hostname in host entry %q is empty", host) } if values[1] == "" { return errors.Errorf("IP address in host entry %q is empty", host) } fmt.Fprintf(buf, "%s\t%s\n", values[1], values[0]) } return buf.Flush() } func addHostsToFile(hosts []string, filename string) error { if len(hosts) == 0 { return nil } file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, os.ModeAppend) if err != nil { return err } defer file.Close() return addHosts(hosts, file) } func addCommonOptsToSpec(commonOpts *CommonBuildOptions, g *generate.Generator) error { // Resources - CPU if commonOpts.CPUPeriod != 0 { g.SetLinuxResourcesCPUPeriod(commonOpts.CPUPeriod) } if commonOpts.CPUQuota != 0 { g.SetLinuxResourcesCPUQuota(commonOpts.CPUQuota) } if commonOpts.CPUShares != 0 { g.SetLinuxResourcesCPUShares(commonOpts.CPUShares) } if commonOpts.CPUSetCPUs != "" { g.SetLinuxResourcesCPUCpus(commonOpts.CPUSetCPUs) } if commonOpts.CPUSetMems != "" { g.SetLinuxResourcesCPUMems(commonOpts.CPUSetMems) } // Resources - Memory if commonOpts.Memory != 0 { g.SetLinuxResourcesMemoryLimit(commonOpts.Memory) } if commonOpts.MemorySwap != 0 { g.SetLinuxResourcesMemorySwap(commonOpts.MemorySwap) } // cgroup membership if commonOpts.CgroupParent != "" { g.SetLinuxCgroupsPath(commonOpts.CgroupParent) } // Other process resource limits if err := addRlimits(commonOpts.Ulimit, g); err != nil { return err } logrus.Debugf("Resources: %#v", commonOpts) return nil } func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath string, optionMounts []specs.Mount, bindFiles map[string]string, builtinVolumes, volumeMounts []string, shmSize string, namespaceOptions NamespaceOptions) error { // Start building a new list of mounts. var mounts []specs.Mount haveMount := func(destination string) bool { for _, mount := range mounts { if mount.Destination == destination { // Already have something to mount there. return true } } return false } // Copy mounts from the generated list. mountCgroups := true specMounts := []specs.Mount{} for _, specMount := range spec.Mounts { // Override some of the mounts from the generated list if we're doing different things with namespaces. if specMount.Destination == "/dev/shm" { specMount.Options = []string{"nosuid", "noexec", "nodev", "mode=1777", "size=" + shmSize} user := namespaceOptions.Find(string(specs.UserNamespace)) ipc := namespaceOptions.Find(string(specs.IPCNamespace)) if (ipc == nil || ipc.Host) && (user != nil && !user.Host) { if _, err := os.Stat("/dev/shm"); err != nil && os.IsNotExist(err) { continue } specMount = specs.Mount{ Source: "/dev/shm", Type: "bind", Destination: "/dev/shm", Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, } } } if specMount.Destination == "/dev/mqueue" { user := namespaceOptions.Find(string(specs.UserNamespace)) ipc := namespaceOptions.Find(string(specs.IPCNamespace)) if (ipc == nil || ipc.Host) && (user != nil && !user.Host) { if _, err := os.Stat("/dev/mqueue"); err != nil && os.IsNotExist(err) { continue } specMount = specs.Mount{ Source: "/dev/mqueue", Type: "bind", Destination: "/dev/mqueue", Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev"}, } } } if specMount.Destination == "/sys" { user := namespaceOptions.Find(string(specs.UserNamespace)) net := namespaceOptions.Find(string(specs.NetworkNamespace)) if (net == nil || net.Host) && (user != nil && !user.Host) { mountCgroups = false if _, err := os.Stat("/sys"); err != nil && os.IsNotExist(err) { continue } specMount = specs.Mount{ Source: "/sys", Type: "bind", Destination: "/sys", Options: []string{bind.NoBindOption, "rbind", "nosuid", "noexec", "nodev", "ro"}, } } } specMounts = append(specMounts, specMount) } // Add a mount for the cgroups filesystem, unless we're already // recursively bind mounting all of /sys, in which case we shouldn't // bother with it. sysfsMount := []specs.Mount{} if mountCgroups { sysfsMount = []specs.Mount{{ Destination: "/sys/fs/cgroup", Type: "cgroup", Source: "cgroup", Options: []string{bind.NoBindOption, "nosuid", "noexec", "nodev", "relatime", "ro"}, }} } // Get the list of files we need to bind into the container. bindFileMounts, err := runSetupBoundFiles(bundlePath, bindFiles) if err != nil { return err } // After this point we need to know the per-container persistent storage directory. cdir, err := b.store.ContainerDirectory(b.ContainerID) if err != nil { return errors.Wrapf(err, "error determining work directory for container %q", b.ContainerID) } // Figure out which UID and GID to tell the secrets package to use // for files that it creates. rootUID, rootGID, err := util.GetHostRootIDs(spec) if err != nil { return err } // Get the list of secrets mounts. secretMounts := secrets.SecretMountsWithUIDGID(b.MountLabel, cdir, b.DefaultMountsFilePath, cdir, int(rootUID), int(rootGID)) // Add temporary copies of the contents of volume locations at the // volume locations, unless we already have something there. copyWithTar := b.copyWithTar(nil, nil) builtins, err := runSetupBuiltinVolumes(b.MountLabel, mountPoint, cdir, copyWithTar, builtinVolumes) if err != nil { return err } // Get the list of explicitly-specified volume mounts. volumes, err := runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts) if err != nil { return err } // Add them all, in the preferred order, except where they conflict with something that was previously added. for _, mount := range append(append(append(append(append(volumes, builtins...), secretMounts...), bindFileMounts...), specMounts...), sysfsMount...) { if haveMount(mount.Destination) { // Already mounting something there, no need to bother with this one. continue } // Add the mount. mounts = append(mounts, mount) } // Set the list in the spec. spec.Mounts = mounts return nil } func runSetupBoundFiles(bundlePath string, bindFiles map[string]string) (mounts []specs.Mount, err error) { for dest, src := range bindFiles { options := []string{"rbind"} if strings.HasPrefix(src, bundlePath) { options = append(options, bind.NoBindOption) } mounts = append(mounts, specs.Mount{ Source: src, Destination: dest, Type: "bind", Options: options, }) } return mounts, nil } func runSetupBuiltinVolumes(mountLabel, mountPoint, containerDir string, copyWithTar func(srcPath, dstPath string) error, builtinVolumes []string) ([]specs.Mount, error) { var mounts []specs.Mount // Add temporary copies of the contents of volume locations at the // volume locations, unless we already have something there. for _, volume := range builtinVolumes { subdir := digest.Canonical.FromString(volume).Hex() volumePath := filepath.Join(containerDir, "buildah-volumes", subdir) // If we need to, initialize the volume path's initial contents. if _, err := os.Stat(volumePath); os.IsNotExist(err) { if err = os.MkdirAll(volumePath, 0755); err != nil { return nil, errors.Wrapf(err, "error creating directory %q for volume %q", volumePath, volume) } if err = label.Relabel(volumePath, mountLabel, false); err != nil { return nil, errors.Wrapf(err, "error relabeling directory %q for volume %q", volumePath, volume) } srcPath := filepath.Join(mountPoint, volume) if err = copyWithTar(srcPath, volumePath); err != nil && !os.IsNotExist(err) { return nil, errors.Wrapf(err, "error populating directory %q for volume %q using contents of %q", volumePath, volume, srcPath) } } // Add the bind mount. mounts = append(mounts, specs.Mount{ Source: volumePath, Destination: volume, Type: "bind", Options: []string{"bind"}, }) } return mounts, nil } func runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount) ([]specs.Mount, error) { var mounts []specs.Mount parseMount := func(host, container string, options []string) (specs.Mount, error) { var foundrw, foundro, foundz, foundZ bool var rootProp string for _, opt := range options { switch opt { case "rw": foundrw = true case "ro": foundro = true case "z": foundz = true case "Z": foundZ = true case "private", "rprivate", "slave", "rslave", "shared", "rshared": rootProp = opt } } if !foundrw && !foundro { options = append(options, "rw") } if foundz { if err := label.Relabel(host, mountLabel, true); err != nil { return specs.Mount{}, errors.Wrapf(err, "relabeling %q failed", host) } } if foundZ { if err := label.Relabel(host, mountLabel, false); err != nil { return specs.Mount{}, errors.Wrapf(err, "relabeling %q failed", host) } } if rootProp == "" { options = append(options, "private") } return specs.Mount{ Destination: container, Type: "bind", Source: host, Options: options, }, nil } // Bind mount volumes specified for this particular Run() invocation for _, i := range optionMounts { mount, err := parseMount(i.Source, i.Destination, append(i.Options, "rbind")) if err != nil { return nil, err } mounts = append(mounts, mount) } // Bind mount volumes given by the user when the container was created for _, i := range volumeMounts { var options []string spliti := strings.Split(i, ":") if len(spliti) > 2 { options = strings.Split(spliti[2], ",") } options = append(options, "rbind") mount, err := parseMount(spliti[0], spliti[1], options) if err != nil { return nil, err } mounts = append(mounts, mount) } return mounts, nil } // addNetworkConfig copies files from host and sets them up to bind mount into container func (b *Builder) addNetworkConfig(rdir, hostPath string, chownOpts *idtools.IDPair) (string, error) { copyFileWithTar := b.copyFileWithTar(chownOpts, nil) cfile := filepath.Join(rdir, filepath.Base(hostPath)) if err := copyFileWithTar(hostPath, cfile); err != nil { return "", errors.Wrapf(err, "error copying %q for container %q", cfile, b.ContainerID) } if err := label.Relabel(cfile, b.MountLabel, false); err != nil { return "", errors.Wrapf(err, "error relabeling %q in container %q", cfile, b.ContainerID) } return cfile, nil } func setupMaskedPaths(g *generate.Generator) { for _, mp := range []string{ "/proc/acpi", "/proc/kcore", "/proc/keys", "/proc/latency_stats", "/proc/timer_list", "/proc/timer_stats", "/proc/sched_debug", "/proc/scsi", "/sys/firmware", } { g.AddLinuxMaskedPaths(mp) } } func setupReadOnlyPaths(g *generate.Generator) { for _, rp := range []string{ "/proc/asound", "/proc/bus", "/proc/fs", "/proc/irq", "/proc/sys", "/proc/sysrq-trigger", } { g.AddLinuxReadonlyPaths(rp) } } func setupCapAdd(g *generate.Generator, caps ...string) error { for _, cap := range caps { if err := g.AddProcessCapabilityBounding(cap); err != nil { return errors.Wrapf(err, "error adding %q to the bounding capability set", cap) } if err := g.AddProcessCapabilityEffective(cap); err != nil { return errors.Wrapf(err, "error adding %q to the effective capability set", cap) } if err := g.AddProcessCapabilityInheritable(cap); err != nil { return errors.Wrapf(err, "error adding %q to the inheritable capability set", cap) } if err := g.AddProcessCapabilityPermitted(cap); err != nil { return errors.Wrapf(err, "error adding %q to the permitted capability set", cap) } if err := g.AddProcessCapabilityAmbient(cap); err != nil { return errors.Wrapf(err, "error adding %q to the ambient capability set", cap) } } return nil } func setupCapDrop(g *generate.Generator, caps ...string) error { for _, cap := range caps { if err := g.DropProcessCapabilityBounding(cap); err != nil { return errors.Wrapf(err, "error removing %q from the bounding capability set", cap) } if err := g.DropProcessCapabilityEffective(cap); err != nil { return errors.Wrapf(err, "error removing %q from the effective capability set", cap) } if err := g.DropProcessCapabilityInheritable(cap); err != nil { return errors.Wrapf(err, "error removing %q from the inheritable capability set", cap) } if err := g.DropProcessCapabilityPermitted(cap); err != nil { return errors.Wrapf(err, "error removing %q from the permitted capability set", cap) } if err := g.DropProcessCapabilityAmbient(cap); err != nil { return errors.Wrapf(err, "error removing %q from the ambient capability set", cap) } } return nil } func setupCapabilities(g *generate.Generator, firstAdds, firstDrops, secondAdds, secondDrops []string) error { g.ClearProcessCapabilities() if err := setupCapAdd(g, util.DefaultCapabilities...); err != nil { return err } if err := setupCapAdd(g, firstAdds...); err != nil { return err } if err := setupCapDrop(g, firstDrops...); err != nil { return err } if err := setupCapAdd(g, secondAdds...); err != nil { return err } if err := setupCapDrop(g, secondDrops...); err != nil { return err } return nil } func setupTerminal(g *generate.Generator, terminalPolicy TerminalPolicy, terminalSize *specs.Box) { switch terminalPolicy { case DefaultTerminal: onTerminal := terminal.IsTerminal(unix.Stdin) && terminal.IsTerminal(unix.Stdout) && terminal.IsTerminal(unix.Stderr) if onTerminal { logrus.Debugf("stdio is a terminal, defaulting to using a terminal") } else { logrus.Debugf("stdio is not a terminal, defaulting to not using a terminal") } g.SetProcessTerminal(onTerminal) case WithTerminal: g.SetProcessTerminal(true) case WithoutTerminal: g.SetProcessTerminal(false) } if terminalSize != nil { g.SetProcessConsoleSize(terminalSize.Width, terminalSize.Height) } } func setupNamespaces(g *generate.Generator, namespaceOptions NamespaceOptions, idmapOptions IDMappingOptions, policy NetworkConfigurationPolicy) (configureNetwork bool, configureNetworks []string, configureUTS bool, err error) { // Set namespace options in the container configuration. hostPidns := false configureUserns := false specifiedNetwork := false for _, namespaceOption := range namespaceOptions { switch namespaceOption.Name { case string(specs.UserNamespace): configureUserns = false if !namespaceOption.Host && namespaceOption.Path == "" { configureUserns = true } case string(specs.PIDNamespace): hostPidns = namespaceOption.Host case string(specs.NetworkNamespace): specifiedNetwork = true configureNetwork = false if !namespaceOption.Host && (namespaceOption.Path == "" || !filepath.IsAbs(namespaceOption.Path)) { if namespaceOption.Path != "" && !filepath.IsAbs(namespaceOption.Path) { configureNetworks = strings.Split(namespaceOption.Path, ",") namespaceOption.Path = "" } configureNetwork = (policy != NetworkDisabled) } case string(specs.UTSNamespace): configureUTS = false if !namespaceOption.Host && namespaceOption.Path == "" { configureUTS = true } } if namespaceOption.Host { if err := g.RemoveLinuxNamespace(namespaceOption.Name); err != nil { return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", namespaceOption.Name) } } else if err := g.AddOrReplaceLinuxNamespace(namespaceOption.Name, namespaceOption.Path); err != nil { if namespaceOption.Path == "" { return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", namespaceOption.Name) } return false, nil, false, errors.Wrapf(err, "error adding %q namespace %q for run", namespaceOption.Name, namespaceOption.Path) } } // If we've got mappings, we're going to have to create a user namespace. if len(idmapOptions.UIDMap) > 0 || len(idmapOptions.GIDMap) > 0 || configureUserns { if hostPidns { return false, nil, false, errors.New("unable to mix host PID namespace with user namespace") } if err := g.AddOrReplaceLinuxNamespace(specs.UserNamespace, ""); err != nil { return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", string(specs.UserNamespace)) } hostUidmap, hostGidmap, err := util.GetHostIDMappings("") if err != nil { return false, nil, false, err } for _, m := range idmapOptions.UIDMap { g.AddLinuxUIDMapping(m.HostID, m.ContainerID, m.Size) } if len(idmapOptions.UIDMap) == 0 { for _, m := range hostUidmap { g.AddLinuxUIDMapping(m.ContainerID, m.ContainerID, m.Size) } } for _, m := range idmapOptions.GIDMap { g.AddLinuxGIDMapping(m.HostID, m.ContainerID, m.Size) } if len(idmapOptions.GIDMap) == 0 { for _, m := range hostGidmap { g.AddLinuxGIDMapping(m.ContainerID, m.ContainerID, m.Size) } } if !specifiedNetwork { if err := g.AddOrReplaceLinuxNamespace(specs.NetworkNamespace, ""); err != nil { return false, nil, false, errors.Wrapf(err, "error adding new %q namespace for run", string(specs.NetworkNamespace)) } configureNetwork = (policy != NetworkDisabled) } } else { if err := g.RemoveLinuxNamespace(specs.UserNamespace); err != nil { return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", string(specs.UserNamespace)) } if !specifiedNetwork { if err := g.RemoveLinuxNamespace(specs.NetworkNamespace); err != nil { return false, nil, false, errors.Wrapf(err, "error removing %q namespace for run", string(specs.NetworkNamespace)) } } } return configureNetwork, configureNetworks, configureUTS, nil } // Search for a command that isn't given as an absolute path using the $PATH // under the rootfs. We can't resolve absolute symbolic links without // chroot()ing, which we may not be able to do, so just accept a link as a // valid resolution. func runLookupPath(g *generate.Generator, command []string) []string { // Look for the configured $PATH. spec := g.Spec() envPath := "" for i := range spec.Process.Env { if strings.HasPrefix(spec.Process.Env[i], "PATH=") { envPath = spec.Process.Env[i] } } // If there is no configured $PATH, supply one. if envPath == "" { defaultPath := "/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin" envPath = "PATH=" + defaultPath g.AddProcessEnv("PATH", defaultPath) } // No command, nothing to do. if len(command) == 0 { return command } // Command is already an absolute path, use it as-is. if filepath.IsAbs(command[0]) { return command } // For each element in the PATH, for _, pathEntry := range filepath.SplitList(envPath[5:]) { // if it's the empty string, it's ".", which is the Cwd, if pathEntry == "" { pathEntry = spec.Process.Cwd } // build the absolute path which it might be, candidate := filepath.Join(pathEntry, command[0]) // check if it's there, if fi, err := os.Lstat(filepath.Join(spec.Root.Path, candidate)); fi != nil && err == nil { // and if it's not a directory, and either a symlink or executable, if !fi.IsDir() && ((fi.Mode()&os.ModeSymlink != 0) || (fi.Mode()&0111 != 0)) { // use that. return append([]string{candidate}, command[1:]...) } } } return command } func (b *Builder) configureUIDGID(g *generate.Generator, mountPoint string, options RunOptions) error { // Set the user UID/GID/supplemental group list/capabilities lists. user, err := b.user(mountPoint, options.User) if err != nil { return err } if err := setupCapabilities(g, b.AddCapabilities, b.DropCapabilities, options.AddCapabilities, options.DropCapabilities); err != nil { return err } g.SetProcessUID(user.UID) g.SetProcessGID(user.GID) for _, gid := range user.AdditionalGids { g.AddProcessAdditionalGid(gid) } // Remove capabilities if not running as root if user.UID != 0 { g.ClearProcessCapabilities() } return nil } func (b *Builder) configureEnvironment(g *generate.Generator, options RunOptions) { g.ClearProcessEnv() for _, envSpec := range append(b.Env(), options.Env...) { env := strings.SplitN(envSpec, "=", 2) if len(env) > 1 { g.AddProcessEnv(env[0], env[1]) } } for src, dest := range b.Args { g.AddProcessEnv(src, dest) } } func (b *Builder) configureNamespaces(g *generate.Generator, options RunOptions) (bool, []string, error) { defaultNamespaceOptions, err := DefaultNamespaceOptions() if err != nil { return false, nil, err } namespaceOptions := defaultNamespaceOptions namespaceOptions.AddOrReplace(b.NamespaceOptions...) namespaceOptions.AddOrReplace(options.NamespaceOptions...) networkPolicy := options.ConfigureNetwork if networkPolicy == NetworkDefault { networkPolicy = b.ConfigureNetwork } configureNetwork, configureNetworks, configureUTS, err := setupNamespaces(g, namespaceOptions, b.IDMappingOptions, networkPolicy) if err != nil { return false, nil, err } if configureUTS { if options.Hostname != "" { g.SetHostname(options.Hostname) } else if b.Hostname() != "" { g.SetHostname(b.Hostname()) } } else { g.SetHostname("") } return configureNetwork, configureNetworks, nil } // Run runs the specified command in the container's root filesystem. func (b *Builder) Run(command []string, options RunOptions) error { p, err := ioutil.TempDir("", Package) if err != nil { return err } // On some hosts like AH, /tmp is a symlink and we need an // absolute path. path, err := filepath.EvalSymlinks(p) if err != nil { return err } logrus.Debugf("using %q to hold bundle data", path) defer func() { if err2 := os.RemoveAll(path); err2 != nil { logrus.Errorf("error removing %q: %v", path, err2) } }() gp, err := generate.New("linux") if err != nil { return err } g := &gp b.configureEnvironment(g, options) if os.Getuid() != 0 { g.RemoveMount("/dev/pts") devPts := specs.Mount{ Destination: "/dev/pts", Type: "devpts", Source: "devpts", Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}, } g.AddMount(devPts) } if b.CommonBuildOpts == nil { return errors.Errorf("Invalid format on container you must recreate the container") } if err := addCommonOptsToSpec(b.CommonBuildOpts, g); err != nil { return err } if options.WorkingDir != "" { g.SetProcessCwd(options.WorkingDir) } else if b.WorkDir() != "" { g.SetProcessCwd(b.WorkDir()) } g.SetProcessSelinuxLabel(b.ProcessLabel) g.SetLinuxMountLabel(b.MountLabel) mountPoint, err := b.Mount(b.MountLabel) if err != nil { return err } defer func() { if err2 := b.Unmount(); err2 != nil { logrus.Errorf("error unmounting container: %v", err2) } }() g.SetRootPath(mountPoint) if len(command) > 0 { command = runLookupPath(g, command) g.SetProcessArgs(command) } else { g.SetProcessArgs(nil) } setupMaskedPaths(g) setupReadOnlyPaths(g) setupTerminal(g, options.Terminal, options.TerminalSize) configureNetwork, configureNetworks, err := b.configureNamespaces(g, options) if err != nil { return err } if err := b.configureUIDGID(g, mountPoint, options); err != nil { return err } g.SetProcessApparmorProfile(b.CommonBuildOpts.ApparmorProfile) // Now grab the spec from the generator. Set the generator to nil so that future contributors // will quickly be able to tell that they're supposed to be modifying the spec directly from here. spec := g.Spec() g = nil // Set the working directory, creating it if we must. if spec.Process.Cwd == "" { spec.Process.Cwd = DefaultWorkingDir } if err = os.MkdirAll(filepath.Join(mountPoint, spec.Process.Cwd), 0755); err != nil { return errors.Wrapf(err, "error ensuring working directory %q exists", spec.Process.Cwd) } // Set the seccomp configuration using the specified profile name. Some syscalls are // allowed if certain capabilities are to be granted (example: CAP_SYS_CHROOT and chroot), // so we sorted out the capabilities lists first. if err = setupSeccomp(spec, b.CommonBuildOpts.SeccompProfilePath); err != nil { return err } // Figure out who owns files that will appear to be owned by UID/GID 0 in the container. rootUID, rootGID, err := util.GetHostRootIDs(spec) if err != nil { return err } rootIDPair := &idtools.IDPair{UID: int(rootUID), GID: int(rootGID)} hostFile, err := b.addNetworkConfig(path, "/etc/hosts", rootIDPair) if err != nil { return err } resolvFile, err := b.addNetworkConfig(path, "/etc/resolv.conf", rootIDPair) if err != nil { return err } if err := addHostsToFile(b.CommonBuildOpts.AddHost, hostFile); err != nil { return err } bindFiles := map[string]string{ "/etc/hosts": hostFile, "/etc/resolv.conf": resolvFile, } err = b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, b.Volumes(), b.CommonBuildOpts.Volumes, b.CommonBuildOpts.ShmSize, append(b.NamespaceOptions, options.NamespaceOptions...)) if err != nil { return errors.Wrapf(err, "error resolving mountpoints for container") } if options.CNIConfigDir == "" { options.CNIConfigDir = b.CNIConfigDir if b.CNIConfigDir == "" { options.CNIConfigDir = util.DefaultCNIConfigDir } } if options.CNIPluginPath == "" { options.CNIPluginPath = b.CNIPluginPath if b.CNIPluginPath == "" { options.CNIPluginPath = util.DefaultCNIPluginPath } } isolation := options.Isolation if isolation == IsolationDefault { isolation = b.Isolation if isolation == IsolationDefault { isolation = IsolationOCI } } switch isolation { case IsolationOCI: err = b.runUsingRuntimeSubproc(options, configureNetwork, configureNetworks, spec, mountPoint, path, Package+"-"+filepath.Base(path)) default: err = errors.Errorf("don't know how to run this command") } return err } type runUsingRuntimeSubprocOptions struct { Options RunOptions Spec *specs.Spec RootPath string BundlePath string ConfigureNetwork bool ConfigureNetworks []string ContainerName string } func (b *Builder) runUsingRuntimeSubproc(options RunOptions, configureNetwork bool, configureNetworks []string, spec *specs.Spec, rootPath, bundlePath, containerName string) (err error) { var confwg sync.WaitGroup config, conferr := json.Marshal(runUsingRuntimeSubprocOptions{ Options: options, Spec: spec, RootPath: rootPath, BundlePath: bundlePath, ConfigureNetwork: configureNetwork, ConfigureNetworks: configureNetworks, ContainerName: containerName, }) if conferr != nil { return errors.Wrapf(conferr, "error encoding configuration for %q", runUsingRuntimeCommand) } cmd := reexec.Command(runUsingRuntimeCommand) cmd.Dir = bundlePath cmd.Stdin = options.Stdin if cmd.Stdin == nil { cmd.Stdin = os.Stdin } cmd.Stdout = options.Stdout if cmd.Stdout == nil { cmd.Stdout = os.Stdout } cmd.Stderr = options.Stderr if cmd.Stderr == nil { cmd.Stderr = os.Stderr } cmd.Env = append(os.Environ(), fmt.Sprintf("LOGLEVEL=%d", logrus.GetLevel())) preader, pwriter, err := os.Pipe() if err != nil { return errors.Wrapf(err, "error creating configuration pipe") } confwg.Add(1) go func() { _, conferr = io.Copy(pwriter, bytes.NewReader(config)) confwg.Done() }() cmd.ExtraFiles = append([]*os.File{preader}, cmd.ExtraFiles...) defer preader.Close() defer pwriter.Close() err = cmd.Run() confwg.Wait() if err == nil { return conferr } return err } func init() { reexec.Register(runUsingRuntimeCommand, runUsingRuntimeMain) } func runUsingRuntimeMain() { var options runUsingRuntimeSubprocOptions // Set logging. if level := os.Getenv("LOGLEVEL"); level != "" { if ll, err := strconv.Atoi(level); err == nil { logrus.SetLevel(logrus.Level(ll)) } } // Unpack our configuration. confPipe := os.NewFile(3, "confpipe") if confPipe == nil { fmt.Fprintf(os.Stderr, "error reading options pipe\n") os.Exit(1) } defer confPipe.Close() if err := json.NewDecoder(confPipe).Decode(&options); err != nil { fmt.Fprintf(os.Stderr, "error decoding options: %v\n", err) os.Exit(1) } // Set ourselves up to read the container's exit status. We're doing this in a child process // so that we won't mess with the setting in a caller of the library. This stubs to OS specific // calls if err := setChildProcess(); err != nil { os.Exit(1) } // Run the container, start to finish. status, err := runUsingRuntime(options.Options, options.ConfigureNetwork, options.ConfigureNetworks, options.Spec, options.RootPath, options.BundlePath, options.ContainerName) if err != nil { fmt.Fprintf(os.Stderr, "error running container: %v\n", err) os.Exit(1) } // Pass the container's exit status back to the caller by exiting with the same status. if status.Exited() { os.Exit(status.ExitStatus()) } else if status.Signaled() { fmt.Fprintf(os.Stderr, "container exited on %s\n", status.Signal()) os.Exit(1) } os.Exit(1) } func runUsingRuntime(options RunOptions, configureNetwork bool, configureNetworks []string, spec *specs.Spec, rootPath, bundlePath, containerName string) (wstatus unix.WaitStatus, err error) { // Lock the caller to a single OS-level thread. runtime.LockOSThread() // Set up bind mounts for things that a namespaced user might not be able to get to directly. unmountAll, err := bind.SetupIntermediateMountNamespace(spec, bundlePath) if unmountAll != nil { defer func() { if err := unmountAll(); err != nil { logrus.Error(err) } }() } if err != nil { return 1, err } // Write the runtime configuration. specbytes, err := json.Marshal(spec) if err != nil { return 1, err } if err = ioutils.AtomicWriteFile(filepath.Join(bundlePath, "config.json"), specbytes, 0600); err != nil { return 1, errors.Wrapf(err, "error storing runtime configuration") } logrus.Debugf("config = %v", string(specbytes)) // Decide which runtime to use. runtime := options.Runtime if runtime == "" { runtime = util.Runtime() } // Default to not specifying a console socket location. var moreCreateArgs []string // Default to just passing down our stdio. getCreateStdio := func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { return os.Stdin, os.Stdout, os.Stderr } // Figure out how we're doing stdio handling, and create pipes and sockets. var stdio sync.WaitGroup var consoleListener *net.UnixListener var errorFds, closeBeforeReadingErrorFds []int stdioPipe := make([][]int, 3) copyConsole := false copyPipes := false finishCopy := make([]int, 2) if err = unix.Pipe(finishCopy); err != nil { return 1, errors.Wrapf(err, "error creating pipe for notifying to stop stdio") } finishedCopy := make(chan struct{}) if spec.Process != nil { if spec.Process.Terminal { copyConsole = true // Create a listening socket for accepting the container's terminal's PTY master. socketPath := filepath.Join(bundlePath, "console.sock") consoleListener, err = net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) if err != nil { return 1, errors.Wrapf(err, "error creating socket to receive terminal descriptor") } // Add console socket arguments. moreCreateArgs = append(moreCreateArgs, "--console-socket", socketPath) } else { copyPipes = true // Figure out who should own the pipes. uid, gid, err := util.GetHostRootIDs(spec) if err != nil { return 1, err } // Create stdio pipes. if stdioPipe, err = runMakeStdioPipe(int(uid), int(gid)); err != nil { return 1, err } errorFds = []int{stdioPipe[unix.Stdout][0], stdioPipe[unix.Stderr][0]} closeBeforeReadingErrorFds = []int{stdioPipe[unix.Stdout][1], stdioPipe[unix.Stderr][1]} // Set stdio to our pipes. getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { stdin := os.NewFile(uintptr(stdioPipe[unix.Stdin][0]), "/dev/stdin") stdout := os.NewFile(uintptr(stdioPipe[unix.Stdout][1]), "/dev/stdout") stderr := os.NewFile(uintptr(stdioPipe[unix.Stderr][1]), "/dev/stderr") return stdin, stdout, stderr } } } else { if options.Quiet { // Discard stdout. getCreateStdio = func() (io.ReadCloser, io.WriteCloser, io.WriteCloser) { return os.Stdin, nil, os.Stderr } } } // Build the commands that we'll execute. pidFile := filepath.Join(bundlePath, "pid") args := append(append(append(options.Args, "create", "--bundle", bundlePath, "--pid-file", pidFile), moreCreateArgs...), containerName) create := exec.Command(runtime, args...) create.Dir = bundlePath stdin, stdout, stderr := getCreateStdio() create.Stdin, create.Stdout, create.Stderr = stdin, stdout, stderr if create.SysProcAttr == nil { create.SysProcAttr = &syscall.SysProcAttr{} } args = append(options.Args, "start", containerName) start := exec.Command(runtime, args...) start.Dir = bundlePath start.Stderr = os.Stderr args = append(options.Args, "kill", containerName) kill := exec.Command(runtime, args...) kill.Dir = bundlePath kill.Stderr = os.Stderr args = append(options.Args, "delete", containerName) del := exec.Command(runtime, args...) del.Dir = bundlePath del.Stderr = os.Stderr // Actually create the container. err = create.Run() if err != nil { return 1, errors.Wrapf(err, "error creating container for %v: %s", spec.Process.Args, runCollectOutput(errorFds, closeBeforeReadingErrorFds)) } defer func() { err2 := del.Run() if err2 != nil { if err == nil { err = errors.Wrapf(err2, "error deleting container") } else { logrus.Infof("error deleting container: %v", err2) } } }() // Make sure we read the container's exit status when it exits. pidValue, err := ioutil.ReadFile(pidFile) if err != nil { return 1, errors.Wrapf(err, "error reading pid from %q", pidFile) } pid, err := strconv.Atoi(strings.TrimSpace(string(pidValue))) if err != nil { return 1, errors.Wrapf(err, "error parsing pid %s as a number", string(pidValue)) } var reaping sync.WaitGroup reaping.Add(1) go func() { defer reaping.Done() var err error _, err = unix.Wait4(pid, &wstatus, 0, nil) if err != nil { wstatus = 0 logrus.Errorf("error waiting for container child process %d: %v\n", pid, err) } }() if configureNetwork { teardown, err := runConfigureNetwork(options, configureNetworks, pid, containerName, spec.Process.Args) if teardown != nil { defer teardown() } if err != nil { return 1, err } } if copyPipes { // We don't need the ends of the pipes that belong to the container. stdin.Close() if stdout != nil { stdout.Close() } stderr.Close() } // Handle stdio for the container in the background. stdio.Add(1) go runCopyStdio(&stdio, copyPipes, stdioPipe, copyConsole, consoleListener, finishCopy, finishedCopy, spec) // Start the container. err = start.Run() if err != nil { return 1, errors.Wrapf(err, "error starting container") } stopped := false defer func() { if !stopped { err2 := kill.Run() if err2 != nil { if err == nil { err = errors.Wrapf(err2, "error stopping container") } else { logrus.Infof("error stopping container: %v", err2) } } } }() // Wait for the container to exit. for { now := time.Now() var state specs.State args = append(options.Args, "state", containerName) stat := exec.Command(runtime, args...) stat.Dir = bundlePath stat.Stderr = os.Stderr stateOutput, stateErr := stat.Output() if stateErr != nil { return 1, errors.Wrapf(stateErr, "error reading container state") } if err = json.Unmarshal(stateOutput, &state); err != nil { return 1, errors.Wrapf(stateErr, "error parsing container state %q", string(stateOutput)) } switch state.Status { case "running": case "stopped": stopped = true default: return 1, errors.Errorf("container status unexpectedly changed to %q", state.Status) } if stopped { break } select { case <-finishedCopy: stopped = true case <-time.After(time.Until(now.Add(100 * time.Millisecond))): continue } if stopped { break } } // Close the writing end of the stop-handling-stdio notification pipe. unix.Close(finishCopy[1]) // Wait for the stdio copy goroutine to flush. stdio.Wait() // Wait until we finish reading the exit status. reaping.Wait() return wstatus, nil } func runCollectOutput(fds, closeBeforeReadingFds []int) string { for _, fd := range closeBeforeReadingFds { unix.Close(fd) } var b bytes.Buffer buf := make([]byte, 8192) for _, fd := range fds { nread, err := unix.Read(fd, buf) if err != nil { if errno, isErrno := err.(syscall.Errno); isErrno { switch errno { default: logrus.Errorf("error reading from pipe %d: %v", fd, err) case syscall.EINTR, syscall.EAGAIN: } } else { logrus.Errorf("unable to wait for data from pipe %d: %v", fd, err) } continue } for nread > 0 { r := buf[:nread] if nwritten, err := b.Write(r); err != nil || nwritten != len(r) { if nwritten != len(r) { logrus.Errorf("error buffering data from pipe %d: %v", fd, err) break } } nread, err = unix.Read(fd, buf) if err != nil { if errno, isErrno := err.(syscall.Errno); isErrno { switch errno { default: logrus.Errorf("error reading from pipe %d: %v", fd, err) case syscall.EINTR, syscall.EAGAIN: } } else { logrus.Errorf("unable to wait for data from pipe %d: %v", fd, err) } break } } } return b.String() } func runConfigureNetwork(options RunOptions, configureNetworks []string, pid int, containerName string, command []string) (teardown func(), err error) { var netconf, undo []*libcni.NetworkConfigList // Scan for CNI configuration files. confdir := options.CNIConfigDir files, err := libcni.ConfFiles(confdir, []string{".conf"}) if err != nil { return nil, errors.Wrapf(err, "error finding CNI networking configuration files named *.conf in directory %q", confdir) } lists, err := libcni.ConfFiles(confdir, []string{".conflist"}) if err != nil { return nil, errors.Wrapf(err, "error finding CNI networking configuration list files named *.conflist in directory %q", confdir) } logrus.Debugf("CNI network configuration file list: %#v", append(files, lists...)) // Read the CNI configuration files. for _, file := range files { nc, err := libcni.ConfFromFile(file) if err != nil { return nil, errors.Wrapf(err, "error loading networking configuration from file %q for %v", file, command) } if len(configureNetworks) > 0 && nc.Network != nil && (nc.Network.Name == "" || !util.StringInSlice(nc.Network.Name, configureNetworks)) { if nc.Network.Name == "" { logrus.Debugf("configuration in %q has no name, skipping it", file) } else { logrus.Debugf("configuration in %q has name %q, skipping it", file, nc.Network.Name) } continue } cl, err := libcni.ConfListFromConf(nc) if err != nil { return nil, errors.Wrapf(err, "error converting networking configuration from file %q for %v", file, command) } logrus.Debugf("using network configuration from %q", file) netconf = append(netconf, cl) } for _, list := range lists { cl, err := libcni.ConfListFromFile(list) if err != nil { return nil, errors.Wrapf(err, "error loading networking configuration list from file %q for %v", list, command) } if len(configureNetworks) > 0 && (cl.Name == "" || !util.StringInSlice(cl.Name, configureNetworks)) { if cl.Name == "" { logrus.Debugf("configuration list in %q has no name, skipping it", list) } else { logrus.Debugf("configuration list in %q has name %q, skipping it", list, cl.Name) } continue } logrus.Debugf("using network configuration list from %q", list) netconf = append(netconf, cl) } // Make sure we can access the container's network namespace, // even after it exits, to successfully tear down the // interfaces. Ensure this by opening a handle to the network // namespace, and using our copy to both configure and // deconfigure it. netns := fmt.Sprintf("/proc/%d/ns/net", pid) netFD, err := unix.Open(netns, unix.O_RDONLY, 0) if err != nil { return nil, errors.Wrapf(err, "error opening network namespace for %v", command) } mynetns := fmt.Sprintf("/proc/%d/fd/%d", unix.Getpid(), netFD) // Build our search path for the plugins. pluginPaths := strings.Split(options.CNIPluginPath, string(os.PathListSeparator)) cni := libcni.CNIConfig{Path: pluginPaths} // Configure the interfaces. rtconf := make(map[*libcni.NetworkConfigList]*libcni.RuntimeConf) teardown = func() { for _, nc := range undo { if err = cni.DelNetworkList(nc, rtconf[nc]); err != nil { logrus.Errorf("error cleaning up network %v for %v: %v", rtconf[nc].IfName, command, err) } } unix.Close(netFD) } for i, nc := range netconf { // Build the runtime config for use with this network configuration. rtconf[nc] = &libcni.RuntimeConf{ ContainerID: containerName, NetNS: mynetns, IfName: fmt.Sprintf("if%d", i), Args: [][2]string{}, CapabilityArgs: map[string]interface{}{}, } // Bring it up. _, err := cni.AddNetworkList(nc, rtconf[nc]) if err != nil { return teardown, errors.Wrapf(err, "error configuring network list %v for %v", rtconf[nc].IfName, command) } // Add it to the list of networks to take down when the container process exits. undo = append([]*libcni.NetworkConfigList{nc}, undo...) } return teardown, nil } func runCopyStdio(stdio *sync.WaitGroup, copyPipes bool, stdioPipe [][]int, copyConsole bool, consoleListener *net.UnixListener, finishCopy []int, finishedCopy chan struct{}, spec *specs.Spec) { defer func() { unix.Close(finishCopy[0]) if copyPipes { unix.Close(stdioPipe[unix.Stdin][1]) unix.Close(stdioPipe[unix.Stdout][0]) unix.Close(stdioPipe[unix.Stderr][0]) } stdio.Done() finishedCopy <- struct{}{} }() // Map describing where data on an incoming descriptor should go. relayMap := make(map[int]int) // Map describing incoming and outgoing descriptors. readDesc := make(map[int]string) writeDesc := make(map[int]string) // Buffers. relayBuffer := make(map[int]*bytes.Buffer) // Set up the terminal descriptor or pipes for polling. if copyConsole { // Accept a connection over our listening socket. fd, err := runAcceptTerminal(consoleListener, spec.Process.ConsoleSize) if err != nil { logrus.Errorf("%v", err) return } terminalFD := fd // Input from our stdin, output from the terminal descriptor. relayMap[unix.Stdin] = terminalFD readDesc[unix.Stdin] = "stdin" relayBuffer[terminalFD] = new(bytes.Buffer) writeDesc[terminalFD] = "container terminal input" relayMap[terminalFD] = unix.Stdout readDesc[terminalFD] = "container terminal output" relayBuffer[unix.Stdout] = new(bytes.Buffer) writeDesc[unix.Stdout] = "output" // Set our terminal's mode to raw, to pass handling of special // terminal input to the terminal in the container. if terminal.IsTerminal(unix.Stdin) { if state, err := terminal.MakeRaw(unix.Stdin); err != nil { logrus.Warnf("error setting terminal state: %v", err) } else { defer func() { if err = terminal.Restore(unix.Stdin, state); err != nil { logrus.Errorf("unable to restore terminal state: %v", err) } }() } } } if copyPipes { // Input from our stdin, output from the stdout and stderr pipes. relayMap[unix.Stdin] = stdioPipe[unix.Stdin][1] readDesc[unix.Stdin] = "stdin" relayBuffer[stdioPipe[unix.Stdin][1]] = new(bytes.Buffer) writeDesc[stdioPipe[unix.Stdin][1]] = "container stdin" relayMap[stdioPipe[unix.Stdout][0]] = unix.Stdout readDesc[stdioPipe[unix.Stdout][0]] = "container stdout" relayBuffer[unix.Stdout] = new(bytes.Buffer) writeDesc[unix.Stdout] = "stdout" relayMap[stdioPipe[unix.Stderr][0]] = unix.Stderr readDesc[stdioPipe[unix.Stderr][0]] = "container stderr" relayBuffer[unix.Stderr] = new(bytes.Buffer) writeDesc[unix.Stderr] = "stderr" } // Set our reading descriptors to non-blocking. for fd := range relayMap { if err := unix.SetNonblock(fd, true); err != nil { logrus.Errorf("error setting %s to nonblocking: %v", readDesc[fd], err) return } } // A helper that returns false if err is an error that would cause us // to give up. logIfNotRetryable := func(err error, what string) (retry bool) { if err == nil { return true } if errno, isErrno := err.(syscall.Errno); isErrno { switch errno { case syscall.EINTR, syscall.EAGAIN: return true } } logrus.Error(what) return false } // Pass data back and forth. pollTimeout := -1 for len(relayMap) > 0 { // Start building the list of descriptors to poll. pollFds := make([]unix.PollFd, 0, len(relayMap)+1) // Poll for a notification that we should stop handling stdio. pollFds = append(pollFds, unix.PollFd{Fd: int32(finishCopy[0]), Events: unix.POLLIN | unix.POLLHUP}) // Poll on our reading descriptors. for rfd := range relayMap { pollFds = append(pollFds, unix.PollFd{Fd: int32(rfd), Events: unix.POLLIN | unix.POLLHUP}) } buf := make([]byte, 8192) // Wait for new data from any input descriptor, or a notification that we're done. _, err := unix.Poll(pollFds, pollTimeout) if !logIfNotRetryable(err, fmt.Sprintf("error waiting for stdio/terminal data to relay: %v", err)) { return } removes := make(map[int]struct{}) for _, pollFd := range pollFds { // If this descriptor's just been closed from the other end, mark it for // removal from the set that we're checking for. if pollFd.Revents&unix.POLLHUP == unix.POLLHUP { removes[int(pollFd.Fd)] = struct{}{} } // If the descriptor was closed elsewhere, remove it from our list. if pollFd.Revents&unix.POLLNVAL != 0 { logrus.Debugf("error polling descriptor %d: closed?", pollFd.Fd) removes[int(pollFd.Fd)] = struct{}{} } // If the POLLIN flag isn't set, then there's no data to be read from this descriptor. if pollFd.Revents&unix.POLLIN == 0 { // If we're using pipes and it's our stdin and it's closed, close the writing // end of the corresponding pipe. if copyPipes && int(pollFd.Fd) == unix.Stdin && pollFd.Revents&unix.POLLHUP != 0 { unix.Close(stdioPipe[unix.Stdin][1]) stdioPipe[unix.Stdin][1] = -1 } continue } // Read whatever there is to be read. readFD := int(pollFd.Fd) writeFD, needToRelay := relayMap[readFD] if needToRelay { n, err := unix.Read(readFD, buf) if !logIfNotRetryable(err, fmt.Sprintf("unable to read %s data: %v", readDesc[readFD], err)) { return } // If it's zero-length on our stdin and we're // using pipes, it's an EOF, so close the stdin // pipe's writing end. if n == 0 && copyPipes && int(pollFd.Fd) == unix.Stdin { unix.Close(stdioPipe[unix.Stdin][1]) stdioPipe[unix.Stdin][1] = -1 } if n > 0 { // Buffer the data in case we get blocked on where they need to go. relayBuffer[writeFD].Write(buf[:n]) } } } // Try to drain the output buffers. Set the default timeout // for the next poll() to 100ms if we still have data to write. pollTimeout = -1 for writeFD := range relayBuffer { if relayBuffer[writeFD].Len() > 0 { n, err := unix.Write(writeFD, relayBuffer[writeFD].Bytes()) if !logIfNotRetryable(err, fmt.Sprintf("unable to write %s data: %v", writeDesc[writeFD], err)) { return } if n > 0 { relayBuffer[writeFD].Next(n) } } if relayBuffer[writeFD].Len() > 0 { pollTimeout = 100 } } // Remove any descriptors which we don't need to poll any more from the poll descriptor list. for remove := range removes { delete(relayMap, remove) } // If the we-can-return pipe had anything for us, we're done. for _, pollFd := range pollFds { if int(pollFd.Fd) == finishCopy[0] && pollFd.Revents != 0 { // The pipe is closed, indicating that we can stop now. return } } } } func runAcceptTerminal(consoleListener *net.UnixListener, terminalSize *specs.Box) (int, error) { defer consoleListener.Close() c, err := consoleListener.AcceptUnix() if err != nil { return -1, errors.Wrapf(err, "error accepting socket descriptor connection") } defer c.Close() // Expect a control message over our new connection. b := make([]byte, 8192) oob := make([]byte, 8192) n, oobn, _, _, err := c.ReadMsgUnix(b, oob) if err != nil { return -1, errors.Wrapf(err, "error reading socket descriptor: %v") } if n > 0 { logrus.Debugf("socket descriptor is for %q", string(b[:n])) } if oobn > len(oob) { return -1, errors.Errorf("too much out-of-bounds data (%d bytes)", oobn) } // Parse the control message. scm, err := unix.ParseSocketControlMessage(oob[:oobn]) if err != nil { return -1, errors.Wrapf(err, "error parsing out-of-bound data as a socket control message") } logrus.Debugf("control messages: %v", scm) // Expect to get a descriptor. terminalFD := -1 for i := range scm { fds, err := unix.ParseUnixRights(&scm[i]) if err != nil { return -1, errors.Wrapf(err, "error parsing unix rights control message: %v") } logrus.Debugf("fds: %v", fds) if len(fds) == 0 { continue } terminalFD = fds[0] break } if terminalFD == -1 { return -1, errors.Errorf("unable to read terminal descriptor") } // Set the pseudoterminal's size to the configured size, or our own. winsize := &unix.Winsize{} if terminalSize != nil { // Use configured sizes. winsize.Row = uint16(terminalSize.Height) winsize.Col = uint16(terminalSize.Width) } else { if terminal.IsTerminal(unix.Stdin) { // Use the size of our terminal. if winsize, err = unix.IoctlGetWinsize(unix.Stdin, unix.TIOCGWINSZ); err != nil { logrus.Warnf("error reading size of controlling terminal: %v", err) winsize.Row = 0 winsize.Col = 0 } } } if winsize.Row != 0 && winsize.Col != 0 { if err = unix.IoctlSetWinsize(terminalFD, unix.TIOCSWINSZ, winsize); err != nil { logrus.Warnf("error setting size of container pseudoterminal: %v", err) } // FIXME - if we're connected to a terminal, we should // be passing the updated terminal size down when we // receive a SIGWINCH. } return terminalFD, nil } // Create pipes to use for relaying stdio. func runMakeStdioPipe(uid, gid int) ([][]int, error) { stdioPipe := make([][]int, 3) for i := range stdioPipe { stdioPipe[i] = make([]int, 2) if err := unix.Pipe(stdioPipe[i]); err != nil { return nil, errors.Wrapf(err, "error creating pipe for container FD %d", i) } } if err := unix.Fchown(stdioPipe[unix.Stdin][0], uid, gid); err != nil { return nil, errors.Wrapf(err, "error setting owner of stdin pipe descriptor") } if err := unix.Fchown(stdioPipe[unix.Stdout][1], uid, gid); err != nil { return nil, errors.Wrapf(err, "error setting owner of stdout pipe descriptor") } if err := unix.Fchown(stdioPipe[unix.Stderr][1], uid, gid); err != nil { return nil, errors.Wrapf(err, "error setting owner of stderr pipe descriptor") } return stdioPipe, nil }