diff options
-rw-r--r-- | .papr.yml | 1 | ||||
-rw-r--r-- | Dockerfile.Fedora | 2 | ||||
-rw-r--r-- | cmd/podman/main.go | 5 | ||||
-rw-r--r-- | cmd/podman/ps.go | 13 | ||||
-rw-r--r-- | cmd/podman/rm.go | 23 | ||||
-rw-r--r-- | cmd/podman/shared/container.go | 4 | ||||
-rw-r--r-- | cmd/podman/shared/parallel.go | 91 | ||||
-rw-r--r-- | cmd/podman/stop.go | 20 | ||||
-rw-r--r-- | cmd/podman/utils.go | 66 | ||||
-rw-r--r-- | libpod/container_internal.go | 34 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 92 | ||||
-rw-r--r-- | libpod/networking_linux.go | 30 | ||||
-rw-r--r-- | vendor/github.com/cyphar/filepath-securejoin/LICENSE | 28 | ||||
-rw-r--r-- | vendor/github.com/cyphar/filepath-securejoin/README.md | 65 | ||||
-rw-r--r-- | vendor/github.com/cyphar/filepath-securejoin/join.go | 134 | ||||
-rw-r--r-- | vendor/github.com/cyphar/filepath-securejoin/vendor.conf | 1 | ||||
-rw-r--r-- | vendor/github.com/cyphar/filepath-securejoin/vfs.go | 41 |
17 files changed, 510 insertions, 140 deletions
@@ -122,6 +122,7 @@ packages: - python3-varlink - python3-dateutil - python3-psutil + - https://kojipkgs.fedoraproject.org//packages/runc/1.0.0/55.dev.git578fe65.fc28/x86_64/runc-1.0.0-55.dev.git578fe65.fc28.x86_64.rpm tests: - sed 's/^expand-check.*/expand-check=0/g' -i /etc/selinux/semanage.conf diff --git a/Dockerfile.Fedora b/Dockerfile.Fedora index c83097227..38cd599d4 100644 --- a/Dockerfile.Fedora +++ b/Dockerfile.Fedora @@ -17,7 +17,7 @@ RUN dnf -y install btrfs-progs-devel \ libseccomp-devel \ libselinux-devel \ skopeo-containers \ - runc \ + https://kojipkgs.fedoraproject.org//packages/runc/1.0.0/55.dev.git578fe65.fc28/x86_64/runc-1.0.0-55.dev.git578fe65.fc28.x86_64.rpm \ make \ ostree-devel \ python \ diff --git a/cmd/podman/main.go b/cmd/podman/main.go index d4c8454a8..38eac4504 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -211,6 +211,11 @@ func main() { Value: hooks.DefaultDir, Hidden: true, }, + cli.IntFlag{ + Name: "max-workers", + Usage: "the maximum number of workers for parallel operations", + Hidden: true, + }, cli.StringFlag{ Name: "log-level", Usage: "log messages above specified level: debug, info, warn, error (default), fatal or panic", diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go index a468f6121..d63618e58 100644 --- a/cmd/podman/ps.go +++ b/cmd/podman/ps.go @@ -20,6 +20,7 @@ import ( "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/go-units" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/urfave/cli" "k8s.io/apimachinery/pkg/fields" ) @@ -300,7 +301,13 @@ func psCmd(c *cli.Context) error { outputContainers = []*libpod.Container{latestCtr} } - pss := shared.PBatch(outputContainers, 8, opts) + maxWorkers := shared.Parallelize("ps") + if c.GlobalIsSet("max-workers") { + maxWorkers = c.GlobalInt("max-workers") + } + logrus.Debugf("Setting maximum workers to %d", maxWorkers) + + pss := shared.PBatch(outputContainers, maxWorkers, opts) if opts.Sort != "" { pss, err = sortPsOutput(opts.Sort, pss) if err != nil { @@ -344,7 +351,9 @@ func psCmd(c *cli.Context) error { // Output Namespace headers fmt.Fprintf(w, "\n%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s", hid, hnames, nspid, nscgroup, nsipc, nsmnt, nsnet, nspidns, nsuserns, nsuts) } - + if len(pss) == 0 { + fmt.Fprint(w, "\n") + } // Now iterate each container and output its information for _, container := range pss { diff --git a/cmd/podman/rm.go b/cmd/podman/rm.go index c6641e879..0fb5345ee 100644 --- a/cmd/podman/rm.go +++ b/cmd/podman/rm.go @@ -2,11 +2,11 @@ package main import ( "fmt" - rt "runtime" - "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -48,14 +48,13 @@ func rmCmd(c *cli.Context) error { var ( delContainers []*libpod.Container lastError error - deleteFuncs []workerInput + deleteFuncs []shared.ParallelWorkerInput ) ctx := getContext() if err := validateFlags(c, rmFlags); err != nil { return err } - runtime, err := libpodruntime.GetRuntime(c) if err != nil { return errors.Wrapf(err, "could not get runtime") @@ -69,17 +68,23 @@ func rmCmd(c *cli.Context) error { delContainers, lastError = getAllOrLatestContainers(c, runtime, -1, "all") for _, container := range delContainers { + con := container f := func() error { - return runtime.RemoveContainer(ctx, container, c.Bool("force")) + return runtime.RemoveContainer(ctx, con, c.Bool("force")) } - deleteFuncs = append(deleteFuncs, workerInput{ - containerID: container.ID(), - parallelFunc: f, + deleteFuncs = append(deleteFuncs, shared.ParallelWorkerInput{ + ContainerID: con.ID(), + ParallelFunc: f, }) } + maxWorkers := shared.Parallelize("rm") + if c.GlobalIsSet("max-workers") { + maxWorkers = c.GlobalInt("max-workers") + } + logrus.Debugf("Setting maximum workers to %d", maxWorkers) - deleteErrors := parallelExecuteWorkerPool(rt.NumCPU()*3, deleteFuncs) + deleteErrors := shared.ParallelExecuteWorkerPool(maxWorkers, deleteFuncs) for cid, result := range deleteErrors { if result != nil { fmt.Println(result.Error()) diff --git a/cmd/podman/shared/container.go b/cmd/podman/shared/container.go index 4af737e0a..b847314a4 100644 --- a/cmd/podman/shared/container.go +++ b/cmd/podman/shared/container.go @@ -226,10 +226,10 @@ func NewBatchContainer(ctr *libpod.Container, opts PsOptions) (PsContainerOutput return pso, nil } -type pFunc func() (PsContainerOutput, error) +type batchFunc func() (PsContainerOutput, error) type workerInput struct { - parallelFunc pFunc + parallelFunc batchFunc opts PsOptions cid string job int diff --git a/cmd/podman/shared/parallel.go b/cmd/podman/shared/parallel.go new file mode 100644 index 000000000..03eba2f0b --- /dev/null +++ b/cmd/podman/shared/parallel.go @@ -0,0 +1,91 @@ +package shared + +import ( + "runtime" + "sync" +) + +type pFunc func() error + +// ParallelWorkerInput is a struct used to pass in a slice of parallel funcs to be +// performed on a container ID +type ParallelWorkerInput struct { + ContainerID string + ParallelFunc pFunc +} + +type containerError struct { + ContainerID string + Err error +} + +// ParallelWorker is a "threaded" worker that takes jobs from the channel "queue" +func ParallelWorker(wg *sync.WaitGroup, jobs <-chan ParallelWorkerInput, results chan<- containerError) { + for j := range jobs { + err := j.ParallelFunc() + results <- containerError{ContainerID: j.ContainerID, Err: err} + wg.Done() + } +} + +// ParallelExecuteWorkerPool takes container jobs and performs them in parallel. The worker +// int determines how many workers/threads should be premade. +func ParallelExecuteWorkerPool(workers int, functions []ParallelWorkerInput) map[string]error { + var ( + wg sync.WaitGroup + ) + + resultChan := make(chan containerError, len(functions)) + results := make(map[string]error) + paraJobs := make(chan ParallelWorkerInput, len(functions)) + + // If we have more workers than functions, match up the number of workers and functions + if workers > len(functions) { + workers = len(functions) + } + + // Create the workers + for w := 1; w <= workers; w++ { + go ParallelWorker(&wg, paraJobs, resultChan) + } + + // Add jobs to the workers + for _, j := range functions { + j := j + wg.Add(1) + paraJobs <- j + } + + close(paraJobs) + wg.Wait() + + close(resultChan) + for ctrError := range resultChan { + results[ctrError.ContainerID] = ctrError.Err + } + + return results +} + +// Parallelize provides the maximum number of parallel workers (int) as calculated by a basic +// heuristic. This can be overriden by the --max-workers primary switch to podman. +func Parallelize(job string) int { + numCpus := runtime.NumCPU() + switch job { + case "stop": + if numCpus <= 2 { + return 4 + } else { + return numCpus * 3 + } + case "rm": + if numCpus <= 3 { + return numCpus * 3 + } else { + return numCpus * 4 + } + case "ps": + return 8 + } + return 3 +} diff --git a/cmd/podman/stop.go b/cmd/podman/stop.go index edadbda89..afeb49f76 100644 --- a/cmd/podman/stop.go +++ b/cmd/podman/stop.go @@ -2,12 +2,12 @@ package main import ( "fmt" - rt "runtime" - "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/rootless" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -61,7 +61,7 @@ func stopCmd(c *cli.Context) error { containers, lastError := getAllOrLatestContainers(c, runtime, libpod.ContainerStateRunning, "running") - var stopFuncs []workerInput + var stopFuncs []shared.ParallelWorkerInput for _, ctr := range containers { con := ctr var stopTimeout uint @@ -73,13 +73,19 @@ func stopCmd(c *cli.Context) error { f := func() error { return con.StopWithTimeout(stopTimeout) } - stopFuncs = append(stopFuncs, workerInput{ - containerID: con.ID(), - parallelFunc: f, + stopFuncs = append(stopFuncs, shared.ParallelWorkerInput{ + ContainerID: con.ID(), + ParallelFunc: f, }) } - stopErrors := parallelExecuteWorkerPool(rt.NumCPU()*3, stopFuncs) + maxWorkers := shared.Parallelize("stop") + if c.GlobalIsSet("max-workers") { + maxWorkers = c.GlobalInt("max-workers") + } + logrus.Debugf("Setting maximum workers to %d", maxWorkers) + + stopErrors := shared.ParallelExecuteWorkerPool(maxWorkers, stopFuncs) for cid, result := range stopErrors { if result != nil && result != libpod.ErrCtrStopped { diff --git a/cmd/podman/utils.go b/cmd/podman/utils.go index f9971fd88..afeccb668 100644 --- a/cmd/podman/utils.go +++ b/cmd/podman/utils.go @@ -3,10 +3,6 @@ package main import ( "context" "fmt" - "os" - gosignal "os/signal" - "sync" - "github.com/containers/libpod/libpod" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/term" @@ -15,6 +11,8 @@ import ( "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" "k8s.io/client-go/tools/remotecommand" + "os" + gosignal "os/signal" ) type RawTtyFormatter struct { @@ -209,63 +207,3 @@ func getPodsFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Pod, error } return pods, lastError } - -type pFunc func() error - -type workerInput struct { - containerID string - parallelFunc pFunc -} - -type containerError struct { - containerID string - err error -} - -// worker is a "threaded" worker that takes jobs from the channel "queue" -func worker(wg *sync.WaitGroup, jobs <-chan workerInput, results chan<- containerError) { - for j := range jobs { - err := j.parallelFunc() - results <- containerError{containerID: j.containerID, err: err} - wg.Done() - } -} - -// parallelExecuteWorkerPool takes container jobs and performs them in parallel. The worker -// int is determines how many workers/threads should be premade. -func parallelExecuteWorkerPool(workers int, functions []workerInput) map[string]error { - var ( - wg sync.WaitGroup - ) - - resultChan := make(chan containerError, len(functions)) - results := make(map[string]error) - paraJobs := make(chan workerInput, len(functions)) - - // If we have more workers than functions, match up the number of workers and functions - if workers > len(functions) { - workers = len(functions) - } - - // Create the workers - for w := 1; w <= workers; w++ { - go worker(&wg, paraJobs, resultChan) - } - - // Add jobs to the workers - for _, j := range functions { - j := j - wg.Add(1) - paraJobs <- j - } - - close(paraJobs) - wg.Wait() - - close(resultChan) - for ctrError := range resultChan { - results[ctrError.containerID] = ctrError.err - } - - return results -} diff --git a/libpod/container_internal.go b/libpod/container_internal.go index cb6b940fd..2af216358 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -750,30 +750,31 @@ func (c *Container) restartWithTimeout(ctx context.Context, timeout uint) (err e // TODO: Add ability to override mount label so we can use this for Mount() too // TODO: Can we use this for export? Copying SHM into the export might not be // good -func (c *Container) mountStorage() (err error) { +func (c *Container) mountStorage() (string, error) { + var err error // Container already mounted, nothing to do if c.state.Mounted { - return nil + return c.state.Mountpoint, nil } if !rootless.IsRootless() { // TODO: generalize this mount code so it will mount every mount in ctr.config.Mounts mounted, err := mount.Mounted(c.config.ShmDir) if err != nil { - return errors.Wrapf(err, "unable to determine if %q is mounted", c.config.ShmDir) + return "", errors.Wrapf(err, "unable to determine if %q is mounted", c.config.ShmDir) } if err := os.Chown(c.config.ShmDir, c.RootUID(), c.RootGID()); err != nil { - return errors.Wrapf(err, "failed to chown %s", c.config.ShmDir) + return "", errors.Wrapf(err, "failed to chown %s", c.config.ShmDir) } if !mounted { shmOptions := fmt.Sprintf("mode=1777,size=%d", c.config.ShmSize) if err := c.mountSHM(shmOptions); err != nil { - return err + return "", err } if err := os.Chown(c.config.ShmDir, c.RootUID(), c.RootGID()); err != nil { - return errors.Wrapf(err, "failed to chown %s", c.config.ShmDir) + return "", errors.Wrapf(err, "failed to chown %s", c.config.ShmDir) } } } @@ -782,28 +783,11 @@ func (c *Container) mountStorage() (err error) { if mountPoint == "" { mountPoint, err = c.mount() if err != nil { - return err + return "", err } } - c.state.Mounted = true - c.state.Mountpoint = mountPoint - if c.state.UserNSRoot == "" { - c.state.RealMountpoint = c.state.Mountpoint - } else { - c.state.RealMountpoint = filepath.Join(c.state.UserNSRoot, "mountpoint") - } - - logrus.Debugf("Created root filesystem for container %s at %s", c.ID(), c.state.Mountpoint) - - defer func() { - if err != nil { - if err2 := c.cleanupStorage(); err2 != nil { - logrus.Errorf("Error unmounting storage for container %s: %v", c.ID(), err) - } - } - }() - return c.save() + return mountPoint, nil } // cleanupStorage unmounts and cleans up the container's root filesystem diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index b25645e5c..0a1784ba7 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -12,15 +12,19 @@ import ( "path" "path/filepath" "strings" + "sync" "syscall" "time" cnitypes "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ns" crioAnnotations "github.com/containers/libpod/pkg/annotations" "github.com/containers/libpod/pkg/chrootuser" "github.com/containers/libpod/pkg/criu" "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage/pkg/idtools" + "github.com/cyphar/filepath-securejoin" + "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" @@ -49,24 +53,61 @@ func (c *Container) unmountSHM(mount string) error { // prepare mounts the container and sets up other required resources like net // namespaces func (c *Container) prepare() (err error) { + var ( + wg sync.WaitGroup + netNS ns.NetNS + networkStatus []*cnitypes.Result + createNetNSErr, mountStorageErr error + mountPoint string + saveNetworkStatus bool + ) + + wg.Add(2) + + go func() { + defer wg.Done() + // Set up network namespace if not already set up + if c.config.CreateNetNS && c.state.NetNS == nil && !c.config.PostConfigureNetNS { + saveNetworkStatus = true + netNS, networkStatus, createNetNSErr = c.runtime.createNetNS(c) + } + }() // Mount storage if not mounted - if err := c.mountStorage(); err != nil { - return err - } + go func() { + defer wg.Done() + mountPoint, mountStorageErr = c.mountStorage() + }() - // Set up network namespace if not already set up - if c.config.CreateNetNS && c.state.NetNS == nil && !c.config.PostConfigureNetNS { - if err := c.runtime.createNetNS(c); err != nil { - // Tear down storage before exiting to make sure we - // don't leak mounts - if err2 := c.cleanupStorage(); err2 != nil { - logrus.Errorf("Error cleaning up storage for container %s: %v", c.ID(), err2) - } - return err + wg.Wait() + if createNetNSErr != nil { + if mountStorageErr != nil { + logrus.Error(createNetNSErr) + return mountStorageErr } + return createNetNSErr + } + if mountStorageErr != nil { + return mountStorageErr } - return nil + // Assign NetNS attributes to container + if saveNetworkStatus { + c.state.NetNS = netNS + c.state.NetworkStatus = networkStatus + } + + // Finish up mountStorage + c.state.Mounted = true + c.state.Mountpoint = mountPoint + if c.state.UserNSRoot == "" { + c.state.RealMountpoint = c.state.Mountpoint + } else { + c.state.RealMountpoint = filepath.Join(c.state.UserNSRoot, "mountpoint") + } + + logrus.Debugf("Created root filesystem for container %s at %s", c.ID(), c.state.Mountpoint) + // Save the container + return c.save() } // cleanupNetwork unmounts and cleans up the container's network @@ -104,7 +145,6 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()) } } - // Check if the spec file mounts contain the label Relabel flags z or Z. // If they do, relabel the source directory and then remove the option. for _, m := range g.Mounts() { @@ -197,12 +237,28 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { // Look up and add groups the user belongs to, if a group wasn't directly specified if !rootless.IsRootless() && !strings.Contains(c.config.User, ":") { - groups, err := chrootuser.GetAdditionalGroupsForUser(c.state.Mountpoint, uint64(g.Config.Process.User.UID)) - if err != nil && errors.Cause(err) != chrootuser.ErrNoSuchUser { + var groupDest, passwdDest string + defaultExecUser := user.ExecUser{ + Uid: 0, + Gid: 0, + Home: "/", + } + + // Make sure the /etc/group and /etc/passwd destinations are not a symlink to something naughty + if groupDest, err = securejoin.SecureJoin(c.state.Mountpoint, "/etc/group"); err != nil { + logrus.Debug(err) return nil, err } - for _, gid := range groups { - g.AddProcessAdditionalGid(gid) + if passwdDest, err = securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd"); err != nil { + logrus.Debug(err) + return nil, err + } + execUser, err := user.GetExecUserPath(c.config.User, &defaultExecUser, passwdDest, groupDest) + if err != nil { + return nil, err + } + for _, gid := range execUser.Sgids { + g.AddProcessAdditionalGid(uint32(gid)) } } diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index acb4e2a90..0d9ec2809 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -48,12 +48,12 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port } // Create and configure a new network namespace for a container -func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) (err error) { +func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) ([]*cnitypes.Result, error) { podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctrNS.Path(), ctr.config.Networks, ctr.config.PortMappings, ctr.config.StaticIP) results, err := r.netPlugin.SetUpPod(podNetwork) if err != nil { - return errors.Wrapf(err, "error configuring network namespace for container %s", ctr.ID()) + return nil, errors.Wrapf(err, "error configuring network namespace for container %s", ctr.ID()) } defer func() { if err != nil { @@ -63,15 +63,14 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) (err error) { } }() - ctr.state.NetNS = ctrNS - ctr.state.NetworkStatus = make([]*cnitypes.Result, 0) + networkStatus := make([]*cnitypes.Result, 1) for idx, r := range results { logrus.Debugf("[%d] CNI result: %v", idx, r.String()) resultCurrent, err := cnitypes.GetResult(r) if err != nil { - return errors.Wrapf(err, "error parsing CNI plugin result %q: %v", r.String(), err) + return nil, errors.Wrapf(err, "error parsing CNI plugin result %q: %v", r.String(), err) } - ctr.state.NetworkStatus = append(ctr.state.NetworkStatus, resultCurrent) + networkStatus = append(ctr.state.NetworkStatus, resultCurrent) } // Add firewall rules to ensure the container has network access. @@ -82,18 +81,18 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) (err error) { PrevResult: netStatus, } if err := r.firewallBackend.Add(firewallConf); err != nil { - return errors.Wrapf(err, "error adding firewall rules for container %s", ctr.ID()) + return nil, errors.Wrapf(err, "error adding firewall rules for container %s", ctr.ID()) } } - return nil + return networkStatus, nil } // Create and configure a new network namespace for a container -func (r *Runtime) createNetNS(ctr *Container) (err error) { +func (r *Runtime) createNetNS(ctr *Container) (ns.NetNS, []*cnitypes.Result, error) { ctrNS, err := netns.NewNS() if err != nil { - return errors.Wrapf(err, "error creating network namespace for container %s", ctr.ID()) + return nil, nil, errors.Wrapf(err, "error creating network namespace for container %s", ctr.ID()) } defer func() { if err != nil { @@ -104,7 +103,9 @@ func (r *Runtime) createNetNS(ctr *Container) (err error) { }() logrus.Debugf("Made network namespace at %s for container %s", ctrNS.Path(), ctr.ID()) - return r.configureNetNS(ctr, ctrNS) + + networkStatus, err := r.configureNetNS(ctr, ctrNS) + return ctrNS, networkStatus, err } // Configure the network namespace for a rootless container @@ -173,7 +174,12 @@ func (r *Runtime) setupNetNS(ctr *Container) (err error) { if err != nil { return err } - return r.configureNetNS(ctr, netNS) + networkStatus, err := r.configureNetNS(ctr, netNS) + + // Assign NetNS attributes to container + ctr.state.NetNS = netNS + ctr.state.NetworkStatus = networkStatus + return err } // Join an existing network namespace diff --git a/vendor/github.com/cyphar/filepath-securejoin/LICENSE b/vendor/github.com/cyphar/filepath-securejoin/LICENSE new file mode 100644 index 000000000..bec842f29 --- /dev/null +++ b/vendor/github.com/cyphar/filepath-securejoin/LICENSE @@ -0,0 +1,28 @@ +Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. +Copyright (C) 2017 SUSE LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/cyphar/filepath-securejoin/README.md b/vendor/github.com/cyphar/filepath-securejoin/README.md new file mode 100644 index 000000000..49b2baa9f --- /dev/null +++ b/vendor/github.com/cyphar/filepath-securejoin/README.md @@ -0,0 +1,65 @@ +## `filepath-securejoin` ## + +[![Build Status](https://travis-ci.org/cyphar/filepath-securejoin.svg?branch=master)](https://travis-ci.org/cyphar/filepath-securejoin) + +An implementation of `SecureJoin`, a [candidate for inclusion in the Go +standard library][go#20126]. The purpose of this function is to be a "secure" +alternative to `filepath.Join`, and in particular it provides certain +guarantees that are not provided by `filepath.Join`. + +This is the function prototype: + +```go +func SecureJoin(root, unsafePath string) (string, error) +``` + +This library **guarantees** the following: + +* If no error is set, the resulting string **must** be a child path of + `SecureJoin` and will not contain any symlink path components (they will all + be expanded). + +* When expanding symlinks, all symlink path components **must** be resolved + relative to the provided root. In particular, this can be considered a + userspace implementation of how `chroot(2)` operates on file paths. Note that + these symlinks will **not** be expanded lexically (`filepath.Clean` is not + called on the input before processing). + +* Non-existant path components are unaffected by `SecureJoin` (similar to + `filepath.EvalSymlinks`'s semantics). + +* The returned path will always be `filepath.Clean`ed and thus not contain any + `..` components. + +A (trivial) implementation of this function on GNU/Linux systems could be done +with the following (note that this requires root privileges and is far more +opaque than the implementation in this library, and also requires that +`readlink` is inside the `root` path): + +```go +package securejoin + +import ( + "os/exec" + "path/filepath" +) + +func SecureJoin(root, unsafePath string) (string, error) { + unsafePath = string(filepath.Separator) + unsafePath + cmd := exec.Command("chroot", root, + "readlink", "--canonicalize-missing", "--no-newline", unsafePath) + output, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + expanded := string(output) + return filepath.Join(root, expanded), nil +} +``` + +[go#20126]: https://github.com/golang/go/issues/20126 + +### License ### + +The license of this project is the same as Go, which is a BSD 3-clause license +available in the `LICENSE` file. diff --git a/vendor/github.com/cyphar/filepath-securejoin/join.go b/vendor/github.com/cyphar/filepath-securejoin/join.go new file mode 100644 index 000000000..c4ca3d713 --- /dev/null +++ b/vendor/github.com/cyphar/filepath-securejoin/join.go @@ -0,0 +1,134 @@ +// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. +// Copyright (C) 2017 SUSE LLC. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package securejoin is an implementation of the hopefully-soon-to-be-included +// SecureJoin helper that is meant to be part of the "path/filepath" package. +// The purpose of this project is to provide a PoC implementation to make the +// SecureJoin proposal (https://github.com/golang/go/issues/20126) more +// tangible. +package securejoin + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/pkg/errors" +) + +// ErrSymlinkLoop is returned by SecureJoinVFS when too many symlinks have been +// evaluated in attempting to securely join the two given paths. +var ErrSymlinkLoop = errors.Wrap(syscall.ELOOP, "secure join") + +// IsNotExist tells you if err is an error that implies that either the path +// accessed does not exist (or path components don't exist). This is +// effectively a more broad version of os.IsNotExist. +func IsNotExist(err error) bool { + // If it's a bone-fide ENOENT just bail. + if os.IsNotExist(errors.Cause(err)) { + return true + } + + // Check that it's not actually an ENOTDIR, which in some cases is a more + // convoluted case of ENOENT (usually involving weird paths). + var errno error + switch err := errors.Cause(err).(type) { + case *os.PathError: + errno = err.Err + case *os.LinkError: + errno = err.Err + case *os.SyscallError: + errno = err.Err + } + return errno == syscall.ENOTDIR || errno == syscall.ENOENT +} + +// SecureJoinVFS joins the two given path components (similar to Join) except +// that the returned path is guaranteed to be scoped inside the provided root +// path (when evaluated). Any symbolic links in the path are evaluated with the +// given root treated as the root of the filesystem, similar to a chroot. The +// filesystem state is evaluated through the given VFS interface (if nil, the +// standard os.* family of functions are used). +// +// Note that the guarantees provided by this function only apply if the path +// components in the returned string are not modified (in other words are not +// replaced with symlinks on the filesystem) after this function has returned. +// Such a symlink race is necessarily out-of-scope of SecureJoin. +func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { + // Use the os.* VFS implementation if none was specified. + if vfs == nil { + vfs = osVFS{} + } + + var path bytes.Buffer + n := 0 + for unsafePath != "" { + if n > 255 { + return "", ErrSymlinkLoop + } + + // Next path component, p. + i := strings.IndexRune(unsafePath, filepath.Separator) + var p string + if i == -1 { + p, unsafePath = unsafePath, "" + } else { + p, unsafePath = unsafePath[:i], unsafePath[i+1:] + } + + // Create a cleaned path, using the lexical semantics of /../a, to + // create a "scoped" path component which can safely be joined to fullP + // for evaluation. At this point, path.String() doesn't contain any + // symlink components. + cleanP := filepath.Clean(string(filepath.Separator) + path.String() + p) + if cleanP == string(filepath.Separator) { + path.Reset() + continue + } + fullP := filepath.Clean(root + cleanP) + + // Figure out whether the path is a symlink. + fi, err := vfs.Lstat(fullP) + if err != nil && !IsNotExist(err) { + return "", err + } + // Treat non-existent path components the same as non-symlinks (we + // can't do any better here). + if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 { + path.WriteString(p) + path.WriteRune(filepath.Separator) + continue + } + + // Only increment when we actually dereference a link. + n++ + + // It's a symlink, expand it by prepending it to the yet-unparsed path. + dest, err := vfs.Readlink(fullP) + if err != nil { + return "", err + } + // Absolute symlinks reset any work we've already done. + if filepath.IsAbs(dest) { + path.Reset() + } + unsafePath = dest + string(filepath.Separator) + unsafePath + } + + // We have to clean path.String() here because it may contain '..' + // components that are entirely lexical, but would be misleading otherwise. + // And finally do a final clean to ensure that root is also lexically + // clean. + fullP := filepath.Clean(string(filepath.Separator) + path.String()) + return filepath.Clean(root + fullP), nil +} + +// SecureJoin is a wrapper around SecureJoinVFS that just uses the os.* library +// of functions as the VFS. If in doubt, use this function over SecureJoinVFS. +func SecureJoin(root, unsafePath string) (string, error) { + return SecureJoinVFS(root, unsafePath, nil) +} diff --git a/vendor/github.com/cyphar/filepath-securejoin/vendor.conf b/vendor/github.com/cyphar/filepath-securejoin/vendor.conf new file mode 100644 index 000000000..66bb574b9 --- /dev/null +++ b/vendor/github.com/cyphar/filepath-securejoin/vendor.conf @@ -0,0 +1 @@ +github.com/pkg/errors v0.8.0 diff --git a/vendor/github.com/cyphar/filepath-securejoin/vfs.go b/vendor/github.com/cyphar/filepath-securejoin/vfs.go new file mode 100644 index 000000000..a82a5eae1 --- /dev/null +++ b/vendor/github.com/cyphar/filepath-securejoin/vfs.go @@ -0,0 +1,41 @@ +// Copyright (C) 2017 SUSE LLC. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package securejoin + +import "os" + +// In future this should be moved into a separate package, because now there +// are several projects (umoci and go-mtree) that are using this sort of +// interface. + +// VFS is the minimal interface necessary to use SecureJoinVFS. A nil VFS is +// equivalent to using the standard os.* family of functions. This is mainly +// used for the purposes of mock testing, but also can be used to otherwise use +// SecureJoin with VFS-like system. +type VFS interface { + // Lstat returns a FileInfo describing the named file. If the file is a + // symbolic link, the returned FileInfo describes the symbolic link. Lstat + // makes no attempt to follow the link. These semantics are identical to + // os.Lstat. + Lstat(name string) (os.FileInfo, error) + + // Readlink returns the destination of the named symbolic link. These + // semantics are identical to os.Readlink. + Readlink(name string) (string, error) +} + +// osVFS is the "nil" VFS, in that it just passes everything through to the os +// module. +type osVFS struct{} + +// Lstat returns a FileInfo describing the named file. If the file is a +// symbolic link, the returned FileInfo describes the symbolic link. Lstat +// makes no attempt to follow the link. These semantics are identical to +// os.Lstat. +func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } + +// Readlink returns the destination of the named symbolic link. These +// semantics are identical to os.Readlink. +func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) } |