Clearly, by not attaching the patch that means there are no bugs
(attached for real now) Paul -- .''`. Paul Tagliamonte <paul...@debian.org> | Proud Debian Developer : :' : 4096R / 8F04 9AD8 2C92 066C 7352 D28A 7B58 5B30 807C 2A87 `. `'` http://people.debian.org/~paultag `- http://people.debian.org/~paultag/conduct-statement.txt
diff -Nru docker.io-1.3.1~dfsg1/builder/internals.go docker.io-1.3.2~dfsg1/builder/internals.go --- docker.io-1.3.1~dfsg1/builder/internals.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/builder/internals.go 2014-11-24 12:38:01.000000000 -0500 @@ -22,6 +22,7 @@ "github.com/docker/docker/daemon" imagepkg "github.com/docker/docker/image" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/log" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/promise" @@ -46,7 +47,8 @@ if b.context, err = tarsum.NewTarSum(decompressedStream, true, tarsum.Version0); err != nil { return err } - if err := archive.Untar(b.context, tmpdirPath, nil); err != nil { + + if err := chrootarchive.Untar(b.context, tmpdirPath, nil); err != nil { return err } @@ -620,7 +622,7 @@ } // try to successfully untar the orig - if err := archive.UntarPath(origPath, tarDest); err == nil { + if err := chrootarchive.UntarPath(origPath, tarDest); err == nil { return nil } else if err != io.EOF { log.Debugf("Couldn't untar %s to %s: %s", origPath, tarDest, err) @@ -630,7 +632,7 @@ if err := os.MkdirAll(path.Dir(destPath), 0755); err != nil { return err } - if err := archive.CopyWithTar(origPath, destPath); err != nil { + if err := chrootarchive.CopyWithTar(origPath, destPath); err != nil { return err } @@ -643,7 +645,7 @@ } func copyAsDirectory(source, destination string, destinationExists bool) error { - if err := archive.CopyWithTar(source, destination); err != nil { + if err := chrootarchive.CopyWithTar(source, destination); err != nil { return err } diff -Nru docker.io-1.3.1~dfsg1/CHANGELOG.md docker.io-1.3.2~dfsg1/CHANGELOG.md --- docker.io-1.3.1~dfsg1/CHANGELOG.md 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/CHANGELOG.md 2014-11-24 12:38:01.000000000 -0500 @@ -1,5 +1,21 @@ # Changelog +## 1.3.2 (2014-11-20) + +#### Security +- Fix tar breakout vulnerability +* Extractions are now sandboxed chroot +- Security options are no longer committed to images + +#### Runtime +- Fix deadlock in `docker ps -f exited=1` +- Fix a bug when `--volumes-from` references a container that failed to start + +#### Registry ++ `--insecure-registry` now accepts CIDR notation such as 10.1.0.0/16 +* Private registries whose IPs fall in the 127.0.0.0/8 range do no need the `--insecure-registry` flag +- Skip the experimental registry v2 API when mirroring is enabled + ## 1.3.1 (2014-10-28) #### Security diff -Nru docker.io-1.3.1~dfsg1/daemon/config.go docker.io-1.3.2~dfsg1/daemon/config.go --- docker.io-1.3.1~dfsg1/daemon/config.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/config.go 2014-11-24 12:38:01.000000000 -0500 @@ -56,7 +56,7 @@ flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b") flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking") flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)") - opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback)") + opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)") flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Enable inter-container communication") flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver") flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver") @@ -68,6 +68,14 @@ opts.IPListVar(&config.Dns, []string{"#dns", "-dns"}, "Force Docker to use specific DNS servers") opts.DnsSearchListVar(&config.DnsSearch, []string{"-dns-search"}, "Force Docker to use specific DNS search domains") opts.MirrorListVar(&config.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror") + + // Localhost is by default considered as an insecure registry + // This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker). + // + // TODO: should we deprecate this once it is easier for people to set up a TLS registry or change + // daemon flags on boot2docker? + // If so, do not forget to check the TODO in TestIsSecure + config.InsecureRegistries = append(config.InsecureRegistries, "127.0.0.0/8") } func GetDefaultNetworkMtu() int { diff -Nru docker.io-1.3.1~dfsg1/daemon/daemon.go docker.io-1.3.2~dfsg1/daemon/daemon.go --- docker.io-1.3.1~dfsg1/daemon/daemon.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/daemon.go 2014-11-24 12:38:01.000000000 -0500 @@ -528,10 +528,10 @@ return entrypoint, args } -func parseSecurityOpt(container *Container, config *runconfig.Config) error { +func parseSecurityOpt(container *Container, config *runconfig.HostConfig) error { var ( - label_opts []string - err error + labelOpts []string + err error ) for _, opt := range config.SecurityOpt { @@ -541,7 +541,7 @@ } switch con[0] { case "label": - label_opts = append(label_opts, con[1]) + labelOpts = append(labelOpts, con[1]) case "apparmor": container.AppArmorProfile = con[1] default: @@ -549,7 +549,7 @@ } } - container.ProcessLabel, container.MountLabel, err = label.InitLabels(label_opts) + container.ProcessLabel, container.MountLabel, err = label.InitLabels(labelOpts) return err } @@ -583,7 +583,6 @@ execCommands: newExecStore(), } container.root = daemon.containerRoot(container.ID) - err = parseSecurityOpt(container, config) return container, err } diff -Nru docker.io-1.3.1~dfsg1/daemon/daemon_unit_test.go docker.io-1.3.2~dfsg1/daemon/daemon_unit_test.go --- docker.io-1.3.1~dfsg1/daemon/daemon_unit_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/daemon_unit_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -8,7 +8,7 @@ func TestParseSecurityOpt(t *testing.T) { container := &Container{} - config := &runconfig.Config{} + config := &runconfig.HostConfig{} // test apparmor config.SecurityOpt = []string{"apparmor:test_profile"} diff -Nru docker.io-1.3.1~dfsg1/daemon/execdriver/lxc/init.go docker.io-1.3.2~dfsg1/daemon/execdriver/lxc/init.go --- docker.io-1.3.1~dfsg1/daemon/execdriver/lxc/init.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/execdriver/lxc/init.go 2014-11-24 12:38:01.000000000 -0500 @@ -13,7 +13,7 @@ "strings" "syscall" - "github.com/docker/docker/reexec" + "github.com/docker/docker/pkg/reexec" "github.com/docker/libcontainer/netlink" ) diff -Nru docker.io-1.3.1~dfsg1/daemon/execdriver/native/exec.go docker.io-1.3.2~dfsg1/daemon/execdriver/native/exec.go --- docker.io-1.3.1~dfsg1/daemon/execdriver/native/exec.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/execdriver/native/exec.go 2014-11-24 12:38:01.000000000 -0500 @@ -11,7 +11,7 @@ "runtime" "github.com/docker/docker/daemon/execdriver" - "github.com/docker/docker/reexec" + "github.com/docker/docker/pkg/reexec" "github.com/docker/libcontainer" "github.com/docker/libcontainer/namespaces" ) diff -Nru docker.io-1.3.1~dfsg1/daemon/execdriver/native/init.go docker.io-1.3.2~dfsg1/daemon/execdriver/native/init.go --- docker.io-1.3.1~dfsg1/daemon/execdriver/native/init.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/execdriver/native/init.go 2014-11-24 12:38:01.000000000 -0500 @@ -10,7 +10,7 @@ "path/filepath" "runtime" - "github.com/docker/docker/reexec" + "github.com/docker/docker/pkg/reexec" "github.com/docker/libcontainer" "github.com/docker/libcontainer/namespaces" "github.com/docker/libcontainer/syncpipe" diff -Nru docker.io-1.3.1~dfsg1/daemon/graphdriver/aufs/aufs.go docker.io-1.3.2~dfsg1/daemon/graphdriver/aufs/aufs.go --- docker.io-1.3.1~dfsg1/daemon/graphdriver/aufs/aufs.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/graphdriver/aufs/aufs.go 2014-11-24 12:38:01.000000000 -0500 @@ -32,6 +32,7 @@ "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/log" mountpk "github.com/docker/docker/pkg/mount" "github.com/docker/docker/utils" @@ -304,7 +305,7 @@ } func (a *Driver) applyDiff(id string, diff archive.ArchiveReader) error { - return archive.Untar(diff, path.Join(a.rootPath(), "diff", id), nil) + return chrootarchive.Untar(diff, path.Join(a.rootPath(), "diff", id), nil) } // DiffSize calculates the changes between the specified id diff -Nru docker.io-1.3.1~dfsg1/daemon/graphdriver/aufs/aufs_test.go docker.io-1.3.2~dfsg1/daemon/graphdriver/aufs/aufs_test.go --- docker.io-1.3.1~dfsg1/daemon/graphdriver/aufs/aufs_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/graphdriver/aufs/aufs_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -4,18 +4,24 @@ "crypto/sha256" "encoding/hex" "fmt" - "github.com/docker/docker/daemon/graphdriver" - "github.com/docker/docker/pkg/archive" "io/ioutil" "os" "path" "testing" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" ) var ( tmp = path.Join(os.TempDir(), "aufs-tests", "aufs") ) +func init() { + reexec.Init() +} + func testInit(dir string, t *testing.T) graphdriver.Driver { d, err := Init(dir, nil) if err != nil { diff -Nru docker.io-1.3.1~dfsg1/daemon/graphdriver/fsdiff.go docker.io-1.3.2~dfsg1/daemon/graphdriver/fsdiff.go --- docker.io-1.3.1~dfsg1/daemon/graphdriver/fsdiff.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/graphdriver/fsdiff.go 2014-11-24 12:38:01.000000000 -0500 @@ -5,6 +5,7 @@ "time" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/log" "github.com/docker/docker/utils" @@ -120,7 +121,7 @@ start := time.Now().UTC() log.Debugf("Start untar layer") - if err = archive.ApplyLayer(layerFs, diff); err != nil { + if err = chrootarchive.ApplyLayer(layerFs, diff); err != nil { return } log.Debugf("Untar time: %vs", time.Now().UTC().Sub(start).Seconds()) diff -Nru docker.io-1.3.1~dfsg1/daemon/graphdriver/vfs/driver.go docker.io-1.3.2~dfsg1/daemon/graphdriver/vfs/driver.go --- docker.io-1.3.1~dfsg1/daemon/graphdriver/vfs/driver.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/graphdriver/vfs/driver.go 2014-11-24 12:38:01.000000000 -0500 @@ -8,6 +8,7 @@ "path" "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/libcontainer/label" ) @@ -46,21 +47,6 @@ return false } -func copyDir(src, dst string) error { - argv := make([]string, 0, 4) - - if isGNUcoreutils() { - argv = append(argv, "-aT", "--reflink=auto", src, dst) - } else { - argv = append(argv, "-a", src+"/.", dst+"/.") - } - - if output, err := exec.Command("cp", argv...).CombinedOutput(); err != nil { - return fmt.Errorf("Error VFS copying directory: %s (%s)", err, output) - } - return nil -} - func (d *Driver) Create(id, parent string) error { dir := d.dir(id) if err := os.MkdirAll(path.Dir(dir), 0700); err != nil { @@ -80,7 +66,7 @@ if err != nil { return fmt.Errorf("%s: %s", parent, err) } - if err := copyDir(parentDir, dir); err != nil { + if err := chrootarchive.CopyWithTar(parentDir, dir); err != nil { return err } return nil diff -Nru docker.io-1.3.1~dfsg1/daemon/graphdriver/vfs/vfs_test.go docker.io-1.3.2~dfsg1/daemon/graphdriver/vfs/vfs_test.go --- docker.io-1.3.1~dfsg1/daemon/graphdriver/vfs/vfs_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/graphdriver/vfs/vfs_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -1,10 +1,17 @@ package vfs import ( - "github.com/docker/docker/daemon/graphdriver/graphtest" "testing" + + "github.com/docker/docker/daemon/graphdriver/graphtest" + + "github.com/docker/docker/pkg/reexec" ) +func init() { + reexec.Init() +} + // This avoids creating a new driver for each test if all tests are run // Make sure to put new tests between TestVfsSetup and TestVfsTeardown func TestVfsSetup(t *testing.T) { diff -Nru docker.io-1.3.1~dfsg1/daemon/inspect.go docker.io-1.3.2~dfsg1/daemon/inspect.go --- docker.io-1.3.1~dfsg1/daemon/inspect.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/inspect.go 2014-11-24 12:38:01.000000000 -0500 @@ -47,6 +47,7 @@ out.Set("ProcessLabel", container.ProcessLabel) out.SetJson("Volumes", container.Volumes) out.SetJson("VolumesRW", container.VolumesRW) + out.SetJson("AppArmorProfile", container.AppArmorProfile) if children, err := daemon.Children(container.Name); err == nil { for linkAlias, child := range children { diff -Nru docker.io-1.3.1~dfsg1/daemon/list.go docker.io-1.3.2~dfsg1/daemon/list.go --- docker.io-1.3.1~dfsg1/daemon/list.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/list.go 2014-11-24 12:38:01.000000000 -0500 @@ -93,7 +93,7 @@ if len(filt_exited) > 0 && !container.Running { should_skip := true for _, code := range filt_exited { - if code == container.GetExitCode() { + if code == container.ExitCode { should_skip = false break } diff -Nru docker.io-1.3.1~dfsg1/daemon/networkdriver/portmapper/proxy.go docker.io-1.3.2~dfsg1/daemon/networkdriver/portmapper/proxy.go --- docker.io-1.3.1~dfsg1/daemon/networkdriver/portmapper/proxy.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/networkdriver/portmapper/proxy.go 2014-11-24 12:38:01.000000000 -0500 @@ -14,7 +14,7 @@ "time" "github.com/docker/docker/pkg/proxy" - "github.com/docker/docker/reexec" + "github.com/docker/docker/pkg/reexec" ) const userlandProxyCommandName = "docker-proxy" diff -Nru docker.io-1.3.1~dfsg1/daemon/start.go docker.io-1.3.2~dfsg1/daemon/start.go --- docker.io-1.3.1~dfsg1/daemon/start.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/start.go 2014-11-24 12:38:01.000000000 -0500 @@ -44,6 +44,9 @@ } func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig.HostConfig) error { + if err := parseSecurityOpt(container, hostConfig); err != nil { + return err + } // Validate the HostConfig binds. Make sure that: // the source exists for _, bind := range hostConfig.Binds { diff -Nru docker.io-1.3.1~dfsg1/daemon/volumes.go docker.io-1.3.2~dfsg1/daemon/volumes.go --- docker.io-1.3.1~dfsg1/daemon/volumes.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/daemon/volumes.go 2014-11-24 12:38:01.000000000 -0500 @@ -10,7 +10,7 @@ "syscall" "github.com/docker/docker/daemon/execdriver" - "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/log" "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/volumes" @@ -191,15 +191,20 @@ func (container *Container) applyVolumesFrom() error { volumesFrom := container.hostConfig.VolumesFrom + mountGroups := make([]map[string]*Mount, 0, len(volumesFrom)) + for _, spec := range volumesFrom { - mounts, err := parseVolumesFromSpec(container.daemon, spec) + mountGroup, err := parseVolumesFromSpec(container.daemon, spec) if err != nil { return err } + mountGroups = append(mountGroups, mountGroup) + } + for _, mounts := range mountGroups { for _, mnt := range mounts { mnt.container = container - if err = mnt.initialize(); err != nil { + if err := mnt.initialize(); err != nil { return err } } @@ -302,7 +307,7 @@ if len(srcList) == 0 { // If the source volume is empty copy files from the root into the volume - if err := archive.CopyWithTar(source, destination); err != nil { + if err := chrootarchive.CopyWithTar(source, destination); err != nil { return err } } diff -Nru docker.io-1.3.1~dfsg1/debian/changelog docker.io-1.3.2~dfsg1/debian/changelog --- docker.io-1.3.1~dfsg1/debian/changelog 2014-11-07 15:11:37.000000000 -0500 +++ docker.io-1.3.2~dfsg1/debian/changelog 2014-11-24 19:14:32.000000000 -0500 @@ -1,3 +1,15 @@ +docker.io (1.3.2~dfsg1-1) unstable; urgency=high + + * Severity is set to high due to the sensitive nature of the CVEs this + upload fixes. + * Update to 1.3.2 upstream release + - Fix for CVE-2014-6407 (Archive extraction host privilege escalation) + - Fix for CVE-2014-6408 (Security options applied to image could lead + to container escalation) + * Remove Daniel Mizyrycki from Uploaders. Thanks for all your work! + + -- Paul Tagliamonte <paul...@debian.org> Mon, 24 Nov 2014 19:14:28 -0500 + docker.io (1.3.1~dfsg1-2) unstable; urgency=medium * Remove deprecated /usr/bin/docker.io symlink diff -Nru docker.io-1.3.1~dfsg1/debian/control docker.io-1.3.2~dfsg1/debian/control --- docker.io-1.3.1~dfsg1/debian/control 2014-10-15 21:05:05.000000000 -0400 +++ docker.io-1.3.2~dfsg1/debian/control 2014-11-24 19:14:32.000000000 -0500 @@ -3,7 +3,6 @@ Priority: optional Maintainer: Paul Tagliamonte <paul...@debian.org> Uploaders: Docker Packaging Team <docker-ma...@lists.alioth.debian.org>, - Daniel Mizyrycki <dan...@docker.com>, Tianon Gravi <admwig...@gmail.com>, Johan Euphrosine <pro...@google.com> Build-Depends: bash-completion, diff -Nru docker.io-1.3.1~dfsg1/debian/patches/upstream-patched-archive-tar.patch docker.io-1.3.2~dfsg1/debian/patches/upstream-patched-archive-tar.patch --- docker.io-1.3.1~dfsg1/debian/patches/upstream-patched-archive-tar.patch 2014-10-17 02:34:38.000000000 -0400 +++ docker.io-1.3.2~dfsg1/debian/patches/upstream-patched-archive-tar.patch 2014-11-24 19:14:32.000000000 -0500 @@ -3,7 +3,7 @@ Applied-Upstream: when golang-1.4 is broadly packaged (scheduled to be released 2014-12-01) diff --git a/graph/tags_unit_test.go b/graph/tags_unit_test.go -index e4f1fb8..12db007 100644 +index da51254..de232cb 100644 --- a/graph/tags_unit_test.go +++ b/graph/tags_unit_test.go @@ -7,11 +7,11 @@ import ( @@ -63,7 +63,7 @@ "github.com/docker/docker/builtins" "github.com/docker/docker/daemon" diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go -index 7d9103e..1bc6c0c 100644 +index 155145f..0c41f1b 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -16,7 +16,7 @@ import ( @@ -76,10 +76,10 @@ "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/log" diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go -index b46f953..3a1fc75 100644 +index 7c9db44..39c8caf 100644 --- a/pkg/archive/archive_test.go +++ b/pkg/archive/archive_test.go -@@ -11,7 +11,7 @@ import ( +@@ -12,7 +12,7 @@ import ( "testing" "time" @@ -102,7 +102,7 @@ "github.com/docker/docker/pkg/log" "github.com/docker/docker/pkg/pools" diff --git a/pkg/archive/diff.go b/pkg/archive/diff.go -index 215f62e..47e97bd 100644 +index 5ed1a1d..f20fcb8 100644 --- a/pkg/archive/diff.go +++ b/pkg/archive/diff.go @@ -9,7 +9,7 @@ import ( @@ -114,6 +114,32 @@ "github.com/docker/docker/pkg/pools" ) +diff --git a/pkg/archive/diff_test.go b/pkg/archive/diff_test.go +index 758c411..1af10fe 100644 +--- a/pkg/archive/diff_test.go ++++ b/pkg/archive/diff_test.go +@@ -3,7 +3,7 @@ package archive + import ( + "testing" + +- "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" ++ "archive/tar" + ) + + func TestApplyLayerInvalidFilenames(t *testing.T) { +diff --git a/pkg/archive/utils_test.go b/pkg/archive/utils_test.go +index 3624fe5..8e26a11 100644 +--- a/pkg/archive/utils_test.go ++++ b/pkg/archive/utils_test.go +@@ -9,7 +9,7 @@ import ( + "path/filepath" + "time" + +- "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" ++ "archive/tar" + ) + + var testUntarFns = map[string]func(string, io.Reader) error{ diff --git a/pkg/archive/wrap.go b/pkg/archive/wrap.go index b8b6019..dfb335c 100644 --- a/pkg/archive/wrap.go diff -Nru docker.io-1.3.1~dfsg1/debian/upstream-version-gitcommits docker.io-1.3.2~dfsg1/debian/upstream-version-gitcommits --- docker.io-1.3.1~dfsg1/debian/upstream-version-gitcommits 2014-10-31 17:26:53.000000000 -0400 +++ docker.io-1.3.2~dfsg1/debian/upstream-version-gitcommits 2014-11-24 19:14:32.000000000 -0500 @@ -34,3 +34,4 @@ 1.2.0: fa7b24f 1.3.0: c78088f 1.3.1: 4e9bbfa +1.3.2: 39fa2fa diff -Nru docker.io-1.3.1~dfsg1/docker/docker.go docker.io-1.3.2~dfsg1/docker/docker.go --- docker.io-1.3.1~dfsg1/docker/docker.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/docker/docker.go 2014-11-24 12:38:01.000000000 -0500 @@ -13,7 +13,7 @@ "github.com/docker/docker/api/client" "github.com/docker/docker/dockerversion" flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/reexec" + "github.com/docker/docker/pkg/reexec" "github.com/docker/docker/utils" ) diff -Nru docker.io-1.3.1~dfsg1/Dockerfile docker.io-1.3.2~dfsg1/Dockerfile --- docker.io-1.3.1~dfsg1/Dockerfile 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/Dockerfile 2014-11-24 12:38:01.000000000 -0500 @@ -78,7 +78,7 @@ RUN go get code.google.com/p/go.tools/cmd/cover # TODO replace FPM with some very minimal debhelper stuff -RUN gem install --no-rdoc --no-ri fpm --version 1.0.2 +RUN gem install --no-rdoc --no-ri fpm --version 1.3.2 # Install man page generator RUN mkdir -p /go/src/github.com/cpuguy83 \ diff -Nru docker.io-1.3.1~dfsg1/dockerinit/dockerinit.go docker.io-1.3.2~dfsg1/dockerinit/dockerinit.go --- docker.io-1.3.1~dfsg1/dockerinit/dockerinit.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/dockerinit/dockerinit.go 2014-11-24 12:38:01.000000000 -0500 @@ -3,7 +3,7 @@ import ( _ "github.com/docker/docker/daemon/execdriver/lxc" _ "github.com/docker/docker/daemon/execdriver/native" - "github.com/docker/docker/reexec" + "github.com/docker/docker/pkg/reexec" ) func main() { diff -Nru docker.io-1.3.1~dfsg1/graph/load.go docker.io-1.3.2~dfsg1/graph/load.go --- docker.io-1.3.1~dfsg1/graph/load.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/graph/load.go 2014-11-24 12:38:01.000000000 -0500 @@ -10,6 +10,7 @@ "github.com/docker/docker/engine" "github.com/docker/docker/image" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/log" ) @@ -53,7 +54,7 @@ excludes[i] = k i++ } - if err := archive.Untar(repoFile, repoDir, &archive.TarOptions{Excludes: excludes}); err != nil { + if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{Excludes: excludes}); err != nil { return job.Error(err) } diff -Nru docker.io-1.3.1~dfsg1/graph/pools_test.go docker.io-1.3.2~dfsg1/graph/pools_test.go --- docker.io-1.3.1~dfsg1/graph/pools_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/graph/pools_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -1,6 +1,14 @@ package graph -import "testing" +import ( + "testing" + + "github.com/docker/docker/pkg/reexec" +) + +func init() { + reexec.Init() +} func TestPools(t *testing.T) { s := &TagStore{ diff -Nru docker.io-1.3.1~dfsg1/graph/pull.go docker.io-1.3.2~dfsg1/graph/pull.go --- docker.io-1.3.1~dfsg1/graph/pull.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/graph/pull.go 2014-11-24 12:38:01.000000000 -0500 @@ -113,9 +113,7 @@ return job.Error(err) } - secure := registry.IsSecure(hostname, s.insecureRegistries) - - endpoint, err := registry.NewEndpoint(hostname, secure) + endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries) if err != nil { return job.Error(err) } @@ -139,7 +137,7 @@ mirrors = s.mirrors } - if isOfficial || endpoint.Version == registry.APIVersion2 { + if len(mirrors) == 0 && (isOfficial || endpoint.Version == registry.APIVersion2) { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { return job.Errorf("error updating trust base graph: %s", err) diff -Nru docker.io-1.3.1~dfsg1/graph/push.go docker.io-1.3.2~dfsg1/graph/push.go --- docker.io-1.3.1~dfsg1/graph/push.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/graph/push.go 2014-11-24 12:38:01.000000000 -0500 @@ -214,9 +214,7 @@ return job.Error(err) } - secure := registry.IsSecure(hostname, s.insecureRegistries) - - endpoint, err := registry.NewEndpoint(hostname, secure) + endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries) if err != nil { return job.Error(err) } diff -Nru docker.io-1.3.1~dfsg1/integration/runtime_test.go docker.io-1.3.2~dfsg1/integration/runtime_test.go --- docker.io-1.3.1~dfsg1/integration/runtime_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/integration/runtime_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -22,7 +22,7 @@ "github.com/docker/docker/nat" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/log" - "github.com/docker/docker/reexec" + "github.com/docker/docker/pkg/reexec" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" ) diff -Nru docker.io-1.3.1~dfsg1/integration-cli/docker_cli_ps_test.go docker.io-1.3.2~dfsg1/integration-cli/docker_cli_ps_test.go --- docker.io-1.3.1~dfsg1/integration-cli/docker_cli_ps_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/integration-cli/docker_cli_ps_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -282,3 +282,81 @@ logDone("ps - test ps filter status") } + +func TestPsListContainersFilterExited(t *testing.T) { + deleteAllContainers() + defer deleteAllContainers() + runCmd := exec.Command(dockerBinary, "run", "--name", "zero1", "busybox", "true") + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + t.Fatal(out, err) + } + firstZero, err := getIDByName("zero1") + if err != nil { + t.Fatal(err) + } + + runCmd = exec.Command(dockerBinary, "run", "--name", "zero2", "busybox", "true") + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + t.Fatal(out, err) + } + secondZero, err := getIDByName("zero2") + if err != nil { + t.Fatal(err) + } + + runCmd = exec.Command(dockerBinary, "run", "--name", "nonzero1", "busybox", "false") + out, _, err = runCommandWithOutput(runCmd) + if err == nil { + t.Fatal("Should fail.", out, err) + } + firstNonZero, err := getIDByName("nonzero1") + if err != nil { + t.Fatal(err) + } + + runCmd = exec.Command(dockerBinary, "run", "--name", "nonzero2", "busybox", "false") + out, _, err = runCommandWithOutput(runCmd) + if err == nil { + t.Fatal("Should fail.", out, err) + } + secondNonZero, err := getIDByName("nonzero2") + if err != nil { + t.Fatal(err) + } + + // filter containers by exited=0 + runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--no-trunc", "--filter=exited=0") + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + t.Fatal(out, err) + } + ids := strings.Split(strings.TrimSpace(out), "\n") + if len(ids) != 2 { + t.Fatalf("Should be 2 zero exited containerst got %d", len(ids)) + } + if ids[0] != secondZero { + t.Fatalf("First in list should be %q, got %q", secondZero, ids[0]) + } + if ids[1] != firstZero { + t.Fatalf("Second in list should be %q, got %q", firstZero, ids[1]) + } + + runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--no-trunc", "--filter=exited=1") + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + t.Fatal(out, err) + } + ids = strings.Split(strings.TrimSpace(out), "\n") + if len(ids) != 2 { + t.Fatalf("Should be 2 zero exited containerst got %d", len(ids)) + } + if ids[0] != secondNonZero { + t.Fatalf("First in list should be %q, got %q", secondNonZero, ids[0]) + } + if ids[1] != firstNonZero { + t.Fatalf("Second in list should be %q, got %q", firstNonZero, ids[1]) + } + logDone("ps - test ps filter exited") +} diff -Nru docker.io-1.3.1~dfsg1/integration-cli/docker_cli_start_test.go docker.io-1.3.2~dfsg1/integration-cli/docker_cli_start_test.go --- docker.io-1.3.1~dfsg1/integration-cli/docker_cli_start_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/integration-cli/docker_cli_start_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -2,6 +2,7 @@ import ( "os/exec" + "strings" "testing" "time" ) @@ -36,3 +37,31 @@ logDone("start - error on start with attach exits") } + +// gh#8726: a failed Start() breaks --volumes-from on subsequent Start()'s +func TestStartVolumesFromFailsCleanly(t *testing.T) { + defer deleteAllContainers() + + // Create the first data volume + cmd(t, "run", "-d", "--name", "data_before", "-v", "/foo", "busybox") + + // Expect this to fail because the data test after contaienr doesn't exist yet + if _, err := runCommand(exec.Command(dockerBinary, "run", "-d", "--name", "consumer", "--volumes-from", "data_before", "--volumes-from", "data_after", "busybox")); err == nil { + t.Fatal("Expected error but got none") + } + + // Create the second data volume + cmd(t, "run", "-d", "--name", "data_after", "-v", "/bar", "busybox") + + // Now, all the volumes should be there + cmd(t, "start", "consumer") + + // Check that we have the volumes we want + out, _, _ := cmd(t, "inspect", "--format='{{ len .Volumes }}'", "consumer") + n_volumes := strings.Trim(out, " \r\n'") + if n_volumes != "2" { + t.Fatalf("Missing volumes: expected 2, got %s", n_volumes) + } + + logDone("start - missing containers in --volumes-from did not affect subsequent runs") +} diff -Nru docker.io-1.3.1~dfsg1/pkg/archive/archive.go docker.io-1.3.2~dfsg1/pkg/archive/archive.go --- docker.io-1.3.1~dfsg1/pkg/archive/archive.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/pkg/archive/archive.go 2014-11-24 12:38:01.000000000 -0500 @@ -35,10 +35,22 @@ Compression Compression NoLchown bool } + + // Archiver allows the reuse of most utility functions of this package + // with a pluggable Untar function. + Archiver struct { + Untar func(io.Reader, string, *TarOptions) error + } + + // breakoutError is used to differentiate errors related to breaking out + // When testing archive breakout in the unit tests, this error is expected + // in order for the test to pass. + breakoutError error ) var ( ErrNotImplemented = errors.New("Function not implemented") + defaultArchiver = &Archiver{Untar} ) const ( @@ -263,11 +275,25 @@ } case tar.TypeLink: - if err := os.Link(filepath.Join(extractDir, hdr.Linkname), path); err != nil { + targetPath := filepath.Join(extractDir, hdr.Linkname) + // check for hardlink breakout + if !strings.HasPrefix(targetPath, extractDir) { + return breakoutError(fmt.Errorf("invalid hardlink %q -> %q", targetPath, hdr.Linkname)) + } + if err := os.Link(targetPath, path); err != nil { return err } case tar.TypeSymlink: + // path -> hdr.Linkname = targetPath + // e.g. /extractDir/path/to/symlink -> ../2/file = /extractDir/path/2/file + targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname) + + // the reason we don't need to check symlinks in the path (with FollowSymlinkInScope) is because + // that symlink would first have to be created, which would be caught earlier, at this very check: + if !strings.HasPrefix(targetPath, extractDir) { + return breakoutError(fmt.Errorf("invalid symlink %q -> %q", path, hdr.Linkname)) + } if err := os.Symlink(hdr.Linkname, path); err != nil { return err } @@ -412,6 +438,8 @@ // identity (uncompressed), gzip, bzip2, xz. // FIXME: specify behavior when target path exists vs. doesn't exist. func Untar(archive io.Reader, dest string, options *TarOptions) error { + dest = filepath.Clean(dest) + if options == nil { options = &TarOptions{} } @@ -449,6 +477,7 @@ } // Normalize name, for safety and for a simple is-root check + // This keeps "../" as-is, but normalizes "/../" to "/" hdr.Name = filepath.Clean(hdr.Name) for _, exclude := range options.Excludes { @@ -469,7 +498,11 @@ } } + // Prevent symlink breakout path := filepath.Join(dest, hdr.Name) + if !strings.HasPrefix(path, dest) { + return breakoutError(fmt.Errorf("%q is outside of %q", path, dest)) + } // If path exits we almost always just want to remove and replace it // The only exception is when it is a directory *and* the file from @@ -508,45 +541,47 @@ return nil } -// TarUntar is a convenience function which calls Tar and Untar, with -// the output of one piped into the other. If either Tar or Untar fails, -// TarUntar aborts and returns the error. -func TarUntar(src string, dst string) error { +func (archiver *Archiver) TarUntar(src, dst string) error { log.Debugf("TarUntar(%s %s)", src, dst) archive, err := TarWithOptions(src, &TarOptions{Compression: Uncompressed}) if err != nil { return err } defer archive.Close() - return Untar(archive, dst, nil) + return archiver.Untar(archive, dst, nil) } -// UntarPath is a convenience function which looks for an archive -// at filesystem path `src`, and unpacks it at `dst`. -func UntarPath(src, dst string) error { +// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other. +// If either Tar or Untar fails, TarUntar aborts and returns the error. +func TarUntar(src, dst string) error { + return defaultArchiver.TarUntar(src, dst) +} + +func (archiver *Archiver) UntarPath(src, dst string) error { archive, err := os.Open(src) if err != nil { return err } defer archive.Close() - if err := Untar(archive, dst, nil); err != nil { + if err := archiver.Untar(archive, dst, nil); err != nil { return err } return nil } -// CopyWithTar creates a tar archive of filesystem path `src`, and -// unpacks it at filesystem path `dst`. -// The archive is streamed directly with fixed buffering and no -// intermediary disk IO. -// -func CopyWithTar(src, dst string) error { +// UntarPath is a convenience function which looks for an archive +// at filesystem path `src`, and unpacks it at `dst`. +func UntarPath(src, dst string) error { + return defaultArchiver.UntarPath(src, dst) +} + +func (archiver *Archiver) CopyWithTar(src, dst string) error { srcSt, err := os.Stat(src) if err != nil { return err } if !srcSt.IsDir() { - return CopyFileWithTar(src, dst) + return archiver.CopyFileWithTar(src, dst) } // Create dst, copy src's content into it log.Debugf("Creating dest directory: %s", dst) @@ -554,16 +589,18 @@ return err } log.Debugf("Calling TarUntar(%s, %s)", src, dst) - return TarUntar(src, dst) + return archiver.TarUntar(src, dst) } -// CopyFileWithTar emulates the behavior of the 'cp' command-line -// for a single file. It copies a regular file from path `src` to -// path `dst`, and preserves all its metadata. -// -// If `dst` ends with a trailing slash '/', the final destination path -// will be `dst/base(src)`. -func CopyFileWithTar(src, dst string) (err error) { +// CopyWithTar creates a tar archive of filesystem path `src`, and +// unpacks it at filesystem path `dst`. +// The archive is streamed directly with fixed buffering and no +// intermediary disk IO. +func CopyWithTar(src, dst string) error { + return defaultArchiver.CopyWithTar(src, dst) +} + +func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { log.Debugf("CopyFileWithTar(%s, %s)", src, dst) srcSt, err := os.Stat(src) if err != nil { @@ -611,7 +648,17 @@ err = er } }() - return Untar(r, filepath.Dir(dst), nil) + return archiver.Untar(r, filepath.Dir(dst), nil) +} + +// CopyFileWithTar emulates the behavior of the 'cp' command-line +// for a single file. It copies a regular file from path `src` to +// path `dst`, and preserves all its metadata. +// +// If `dst` ends with a trailing slash '/', the final destination path +// will be `dst/base(src)`. +func CopyFileWithTar(src, dst string) (err error) { + return defaultArchiver.CopyFileWithTar(src, dst) } // CmdStream executes a command, and returns its stdout as a stream. diff -Nru docker.io-1.3.1~dfsg1/pkg/archive/archive_test.go docker.io-1.3.2~dfsg1/pkg/archive/archive_test.go --- docker.io-1.3.1~dfsg1/pkg/archive/archive_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/pkg/archive/archive_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -8,6 +8,7 @@ "os" "os/exec" "path" + "path/filepath" "testing" "time" @@ -169,7 +170,12 @@ // Failing prevents the archives from being uncompressed during ADD func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) { hdr := tar.Header{Typeflag: tar.TypeXGlobalHeader} - err := createTarFile("pax_global_header", "some_dir", &hdr, nil, true) + tmpDir, err := ioutil.TempDir("", "docker-test-archive-pax-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true) if err != nil { t.Fatal(err) } @@ -242,3 +248,201 @@ os.RemoveAll(target) } } + +func TestUntarInvalidFilenames(t *testing.T) { + for i, headers := range [][]*tar.Header{ + { + { + Name: "../victim/dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { + { + // Note the leading slash + Name: "/../victim/slash-dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("untar", "docker-TestUntarInvalidFilenames", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestUntarInvalidHardlink(t *testing.T) { + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeLink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeLink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (hardlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try reading victim/hello (hardlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try removing victim directory (hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("untar", "docker-TestUntarInvalidHardlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestUntarInvalidSymlink(t *testing.T) { + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeSymlink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeSymlink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try removing victim directory (symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try writing to victim/newdir/newfile with a symlink in the path + { + // this header needs to be before the next one, or else there is an error + Name: "dir/loophole", + Typeflag: tar.TypeSymlink, + Linkname: "../../victim", + Mode: 0755, + }, + { + Name: "dir/loophole/newdir/newfile", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("untar", "docker-TestUntarInvalidSymlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} diff -Nru docker.io-1.3.1~dfsg1/pkg/archive/diff.go docker.io-1.3.2~dfsg1/pkg/archive/diff.go --- docker.io-1.3.1~dfsg1/pkg/archive/diff.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/pkg/archive/diff.go 2014-11-24 12:38:01.000000000 -0500 @@ -24,6 +24,8 @@ // ApplyLayer parses a diff in the standard layer format from `layer`, and // applies it to the directory `dest`. func ApplyLayer(dest string, layer ArchiveReader) error { + dest = filepath.Clean(dest) + // We need to be able to set any perms oldmask := syscall.Umask(0) defer syscall.Umask(oldmask) @@ -93,6 +95,12 @@ path := filepath.Join(dest, hdr.Name) base := filepath.Base(path) + + // Prevent symlink breakout + if !strings.HasPrefix(path, dest) { + return breakoutError(fmt.Errorf("%q is outside of %q", path, dest)) + } + if strings.HasPrefix(base, ".wh.") { originalBase := base[len(".wh."):] originalPath := filepath.Join(filepath.Dir(path), originalBase) diff -Nru docker.io-1.3.1~dfsg1/pkg/archive/diff_test.go docker.io-1.3.2~dfsg1/pkg/archive/diff_test.go --- docker.io-1.3.1~dfsg1/pkg/archive/diff_test.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/archive/diff_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,191 @@ +package archive + +import ( + "testing" + + "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" +) + +func TestApplyLayerInvalidFilenames(t *testing.T) { + for i, headers := range [][]*tar.Header{ + { + { + Name: "../victim/dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { + { + // Note the leading slash + Name: "/../victim/slash-dotdot", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidFilenames", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestApplyLayerInvalidHardlink(t *testing.T) { + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeLink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeLink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (hardlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try reading victim/hello (hardlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // Try removing victim directory (hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeLink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidHardlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} + +func TestApplyLayerInvalidSymlink(t *testing.T) { + for i, headers := range [][]*tar.Header{ + { // try reading victim/hello (../) + { + Name: "dotdot", + Typeflag: tar.TypeSymlink, + Linkname: "../victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (/../) + { + Name: "slash-dotdot", + Typeflag: tar.TypeSymlink, + // Note the leading slash + Linkname: "/../victim/hello", + Mode: 0644, + }, + }, + { // try writing victim/file + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim/file", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "symlink", + Typeflag: tar.TypeSymlink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try reading victim/hello (symlink, hardlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "hardlink", + Typeflag: tar.TypeLink, + Linkname: "loophole-victim/hello", + Mode: 0644, + }, + }, + { // try removing victim directory (symlink) + { + Name: "loophole-victim", + Typeflag: tar.TypeSymlink, + Linkname: "../victim", + Mode: 0755, + }, + { + Name: "loophole-victim", + Typeflag: tar.TypeReg, + Mode: 0644, + }, + }, + } { + if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidSymlink", headers); err != nil { + t.Fatalf("i=%d. %v", i, err) + } + } +} diff -Nru docker.io-1.3.1~dfsg1/pkg/archive/utils_test.go docker.io-1.3.2~dfsg1/pkg/archive/utils_test.go --- docker.io-1.3.1~dfsg1/pkg/archive/utils_test.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/archive/utils_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,166 @@ +package archive + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" +) + +var testUntarFns = map[string]func(string, io.Reader) error{ + "untar": func(dest string, r io.Reader) error { + return Untar(r, dest, nil) + }, + "applylayer": func(dest string, r io.Reader) error { + return ApplyLayer(dest, ArchiveReader(r)) + }, +} + +// testBreakout is a helper function that, within the provided `tmpdir` directory, +// creates a `victim` folder with a generated `hello` file in it. +// `untar` extracts to a directory named `dest`, the tar file created from `headers`. +// +// Here are the tested scenarios: +// - removed `victim` folder (write) +// - removed files from `victim` folder (write) +// - new files in `victim` folder (write) +// - modified files in `victim` folder (write) +// - file in `dest` with same content as `victim/hello` (read) +// +// When using testBreakout make sure you cover one of the scenarios listed above. +func testBreakout(untarFn string, tmpdir string, headers []*tar.Header) error { + tmpdir, err := ioutil.TempDir("", tmpdir) + if err != nil { + return err + } + defer os.RemoveAll(tmpdir) + + dest := filepath.Join(tmpdir, "dest") + if err := os.Mkdir(dest, 0755); err != nil { + return err + } + + victim := filepath.Join(tmpdir, "victim") + if err := os.Mkdir(victim, 0755); err != nil { + return err + } + hello := filepath.Join(victim, "hello") + helloData, err := time.Now().MarshalText() + if err != nil { + return err + } + if err := ioutil.WriteFile(hello, helloData, 0644); err != nil { + return err + } + helloStat, err := os.Stat(hello) + if err != nil { + return err + } + + reader, writer := io.Pipe() + go func() { + t := tar.NewWriter(writer) + for _, hdr := range headers { + t.WriteHeader(hdr) + } + t.Close() + }() + + untar := testUntarFns[untarFn] + if untar == nil { + return fmt.Errorf("could not find untar function %q in testUntarFns", untarFn) + } + if err := untar(dest, reader); err != nil { + if _, ok := err.(breakoutError); !ok { + // If untar returns an error unrelated to an archive breakout, + // then consider this an unexpected error and abort. + return err + } + // Here, untar detected the breakout. + // Let's move on verifying that indeed there was no breakout. + fmt.Printf("breakoutError: %v\n", err) + } + + // Check victim folder + f, err := os.Open(victim) + if err != nil { + // codepath taken if victim folder was removed + return fmt.Errorf("archive breakout: error reading %q: %v", victim, err) + } + defer f.Close() + + // Check contents of victim folder + // + // We are only interested in getting 2 files from the victim folder, because if all is well + // we expect only one result, the `hello` file. If there is a second result, it cannot + // hold the same name `hello` and we assume that a new file got created in the victim folder. + // That is enough to detect an archive breakout. + names, err := f.Readdirnames(2) + if err != nil { + // codepath taken if victim is not a folder + return fmt.Errorf("archive breakout: error reading directory content of %q: %v", victim, err) + } + for _, name := range names { + if name != "hello" { + // codepath taken if new file was created in victim folder + return fmt.Errorf("archive breakout: new file %q", name) + } + } + + // Check victim/hello + f, err = os.Open(hello) + if err != nil { + // codepath taken if read permissions were removed + return fmt.Errorf("archive breakout: could not lstat %q: %v", hello, err) + } + defer f.Close() + b, err := ioutil.ReadAll(f) + if err != nil { + return err + } + fi, err := f.Stat() + if err != nil { + return err + } + if helloStat.IsDir() != fi.IsDir() || + // TODO: cannot check for fi.ModTime() change + helloStat.Mode() != fi.Mode() || + helloStat.Size() != fi.Size() || + !bytes.Equal(helloData, b) { + // codepath taken if hello has been modified + return fmt.Errorf("archive breakout: file %q has been modified. Contents: expected=%q, got=%q. FileInfo: expected=%#v, got=%#v.", hello, helloData, b, helloStat, fi) + } + + // Check that nothing in dest/ has the same content as victim/hello. + // Since victim/hello was generated with time.Now(), it is safe to assume + // that any file whose content matches exactly victim/hello, managed somehow + // to access victim/hello. + return filepath.Walk(dest, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + if err != nil { + // skip directory if error + return filepath.SkipDir + } + // enter directory + return nil + } + if err != nil { + // skip file if error + return nil + } + b, err := ioutil.ReadFile(path) + if err != nil { + // Houston, we have a problem. Aborting (space)walk. + return err + } + if bytes.Equal(helloData, b) { + return fmt.Errorf("archive breakout: file %q has been accessed via %q", hello, path) + } + return nil + }) +} diff -Nru docker.io-1.3.1~dfsg1/pkg/chrootarchive/archive.go docker.io-1.3.2~dfsg1/pkg/chrootarchive/archive.go --- docker.io-1.3.1~dfsg1/pkg/chrootarchive/archive.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/chrootarchive/archive.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,90 @@ +package chrootarchive + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "os" + "runtime" + "strings" + "syscall" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" +) + +func untar() { + runtime.LockOSThread() + flag.Parse() + + if err := syscall.Chroot(flag.Arg(0)); err != nil { + fatal(err) + } + if err := syscall.Chdir("/"); err != nil { + fatal(err) + } + options := new(archive.TarOptions) + dec := json.NewDecoder(strings.NewReader(flag.Arg(1))) + if err := dec.Decode(options); err != nil { + fatal(err) + } + if err := archive.Untar(os.Stdin, "/", options); err != nil { + fatal(err) + } + os.Exit(0) +} + +var ( + chrootArchiver = &archive.Archiver{Untar} +) + +func Untar(archive io.Reader, dest string, options *archive.TarOptions) error { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + if err := enc.Encode(options); err != nil { + return fmt.Errorf("Untar json encode: %v", err) + } + if _, err := os.Stat(dest); os.IsNotExist(err) { + if err := os.MkdirAll(dest, 0777); err != nil { + return err + } + } + + cmd := reexec.Command("docker-untar", dest, buf.String()) + cmd.Stdin = archive + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Untar %s %s", err, out) + } + return nil +} + +func TarUntar(src, dst string) error { + return chrootArchiver.TarUntar(src, dst) +} + +// CopyWithTar creates a tar archive of filesystem path `src`, and +// unpacks it at filesystem path `dst`. +// The archive is streamed directly with fixed buffering and no +// intermediary disk IO. +func CopyWithTar(src, dst string) error { + return chrootArchiver.CopyWithTar(src, dst) +} + +// CopyFileWithTar emulates the behavior of the 'cp' command-line +// for a single file. It copies a regular file from path `src` to +// path `dst`, and preserves all its metadata. +// +// If `dst` ends with a trailing slash '/', the final destination path +// will be `dst/base(src)`. +func CopyFileWithTar(src, dst string) (err error) { + return chrootArchiver.CopyFileWithTar(src, dst) +} + +// UntarPath is a convenience function which looks for an archive +// at filesystem path `src`, and unpacks it at `dst`. +func UntarPath(src, dst string) error { + return chrootArchiver.UntarPath(src, dst) +} diff -Nru docker.io-1.3.1~dfsg1/pkg/chrootarchive/archive_test.go docker.io-1.3.2~dfsg1/pkg/chrootarchive/archive_test.go --- docker.io-1.3.1~dfsg1/pkg/chrootarchive/archive_test.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/chrootarchive/archive_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,44 @@ +package chrootarchive + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" +) + +func init() { + reexec.Init() +} + +func TestChrootTarUntar(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-TestChrootTarUntar") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + src := filepath.Join(tmpdir, "src") + if err := os.MkdirAll(src, 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(src, "toto"), []byte("hello toto"), 0644); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(src, "lolo"), []byte("hello lolo"), 0644); err != nil { + t.Fatal(err) + } + stream, err := archive.Tar(src, archive.Uncompressed) + if err != nil { + t.Fatal(err) + } + dest := filepath.Join(tmpdir, "src") + if err := os.MkdirAll(dest, 0700); err != nil { + t.Fatal(err) + } + if err := Untar(stream, dest, &archive.TarOptions{Excludes: []string{"lolo"}}); err != nil { + t.Fatal(err) + } +} diff -Nru docker.io-1.3.1~dfsg1/pkg/chrootarchive/diff.go docker.io-1.3.2~dfsg1/pkg/chrootarchive/diff.go --- docker.io-1.3.1~dfsg1/pkg/chrootarchive/diff.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/chrootarchive/diff.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,46 @@ +package chrootarchive + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "runtime" + "syscall" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/reexec" +) + +func applyLayer() { + runtime.LockOSThread() + flag.Parse() + + if err := syscall.Chroot(flag.Arg(0)); err != nil { + fatal(err) + } + if err := syscall.Chdir("/"); err != nil { + fatal(err) + } + tmpDir, err := ioutil.TempDir("/", "temp-docker-extract") + if err != nil { + fatal(err) + } + os.Setenv("TMPDIR", tmpDir) + if err := archive.ApplyLayer("/", os.Stdin); err != nil { + os.RemoveAll(tmpDir) + fatal(err) + } + os.RemoveAll(tmpDir) + os.Exit(0) +} + +func ApplyLayer(dest string, layer archive.ArchiveReader) error { + cmd := reexec.Command("docker-applyLayer", dest) + cmd.Stdin = layer + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("ApplyLayer %s %s", err, out) + } + return nil +} diff -Nru docker.io-1.3.1~dfsg1/pkg/chrootarchive/init.go docker.io-1.3.2~dfsg1/pkg/chrootarchive/init.go --- docker.io-1.3.1~dfsg1/pkg/chrootarchive/init.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/chrootarchive/init.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,18 @@ +package chrootarchive + +import ( + "fmt" + "os" + + "github.com/docker/docker/pkg/reexec" +) + +func init() { + reexec.Register("docker-untar", untar) + reexec.Register("docker-applyLayer", applyLayer) +} + +func fatal(err error) { + fmt.Fprint(os.Stderr, err) + os.Exit(1) +} diff -Nru docker.io-1.3.1~dfsg1/pkg/reexec/command_linux.go docker.io-1.3.2~dfsg1/pkg/reexec/command_linux.go --- docker.io-1.3.1~dfsg1/pkg/reexec/command_linux.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/reexec/command_linux.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,18 @@ +// +build linux + +package reexec + +import ( + "os/exec" + "syscall" +) + +func Command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + SysProcAttr: &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + }, + } +} diff -Nru docker.io-1.3.1~dfsg1/pkg/reexec/command_unsupported.go docker.io-1.3.2~dfsg1/pkg/reexec/command_unsupported.go --- docker.io-1.3.1~dfsg1/pkg/reexec/command_unsupported.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/reexec/command_unsupported.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,11 @@ +// +build !linux + +package reexec + +import ( + "os/exec" +) + +func Command(args ...string) *exec.Cmd { + return nil +} diff -Nru docker.io-1.3.1~dfsg1/pkg/reexec/MAINTAINERS docker.io-1.3.2~dfsg1/pkg/reexec/MAINTAINERS --- docker.io-1.3.1~dfsg1/pkg/reexec/MAINTAINERS 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/reexec/MAINTAINERS 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1 @@ +Michael Crosby <mich...@docker.com> (@crosbymichael) diff -Nru docker.io-1.3.1~dfsg1/pkg/reexec/README.md docker.io-1.3.2~dfsg1/pkg/reexec/README.md --- docker.io-1.3.1~dfsg1/pkg/reexec/README.md 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/reexec/README.md 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,5 @@ +## reexec + +The `reexec` package facilitates the busybox style reexec of the docker binary that we require because +of the forking limitations of using Go. Handlers can be registered with a name and the argv 0 of +the exec of the binary will be used to find and execute custom init paths. diff -Nru docker.io-1.3.1~dfsg1/pkg/reexec/reexec.go docker.io-1.3.2~dfsg1/pkg/reexec/reexec.go --- docker.io-1.3.1~dfsg1/pkg/reexec/reexec.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/pkg/reexec/reexec.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,42 @@ +package reexec + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +var registeredInitializers = make(map[string]func()) + +// Register adds an initialization func under the specified name +func Register(name string, initializer func()) { + if _, exists := registeredInitializers[name]; exists { + panic(fmt.Sprintf("reexec func already registred under name %q", name)) + } + + registeredInitializers[name] = initializer +} + +// Init is called as the first part of the exec process and returns true if an +// initialization function was called. +func Init() bool { + initializer, exists := registeredInitializers[os.Args[0]] + if exists { + initializer() + + return true + } + return false +} + +// Self returns the path to the current processes binary +func Self() string { + name := os.Args[0] + if filepath.Base(name) == name { + if lp, err := exec.LookPath(name); err == nil { + name = lp + } + } + return name +} diff -Nru docker.io-1.3.1~dfsg1/pkg/symlink/fs.go docker.io-1.3.2~dfsg1/pkg/symlink/fs.go --- docker.io-1.3.1~dfsg1/pkg/symlink/fs.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/pkg/symlink/fs.go 2014-11-24 12:38:01.000000000 -0500 @@ -12,6 +12,12 @@ // FollowSymlink will follow an existing link and scope it to the root // path provided. +// The role of this function is to return an absolute path in the root +// or normalize to the root if the symlink leads to a path which is +// outside of the root. +// Errors encountered while attempting to follow the symlink in path +// will be reported. +// Normalizations to the root don't constitute errors. func FollowSymlinkInScope(link, root string) (string, error) { root, err := filepath.Abs(root) if err != nil { @@ -35,7 +41,6 @@ for _, p := range strings.Split(link, "/") { prev = filepath.Join(prev, p) - prev = filepath.Clean(prev) loopCounter := 0 for { @@ -61,25 +66,36 @@ } return "", err } - if stat.Mode()&os.ModeSymlink == os.ModeSymlink { - dest, err := os.Readlink(prev) - if err != nil { - return "", err - } - if path.IsAbs(dest) { - prev = filepath.Join(root, dest) - } else { - prev, _ = filepath.Abs(prev) - - if prev = filepath.Clean(filepath.Join(filepath.Dir(prev), dest)); len(prev) < len(root) { - prev = filepath.Join(root, filepath.Base(dest)) - } - } - } else { + // let's break if we're not dealing with a symlink + if stat.Mode()&os.ModeSymlink != os.ModeSymlink { break } + + // process the symlink + dest, err := os.Readlink(prev) + if err != nil { + return "", err + } + + if path.IsAbs(dest) { + prev = filepath.Join(root, dest) + } else { + prev, _ = filepath.Abs(prev) + + dir := filepath.Dir(prev) + prev = filepath.Join(dir, dest) + if dir == root && !strings.HasPrefix(prev, root) { + prev = root + } + if len(prev) < len(root) || (len(prev) == len(root) && prev != root) { + prev = filepath.Join(root, filepath.Base(dest)) + } + } } } + if prev == "/" { + prev = root + } return prev, nil } diff -Nru docker.io-1.3.1~dfsg1/pkg/symlink/fs_test.go docker.io-1.3.2~dfsg1/pkg/symlink/fs_test.go --- docker.io-1.3.1~dfsg1/pkg/symlink/fs_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/pkg/symlink/fs_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -46,6 +46,7 @@ if err != nil { t.Fatal(err) } + defer os.RemoveAll(dir) os.Mkdir(filepath.Join(dir, "realdir"), 0700) os.Symlink("realdir", filepath.Join(dir, "linkdir")) @@ -97,25 +98,151 @@ } func TestFollowSymLinkRelativeLinkScope(t *testing.T) { - link := "testdata/fs/a/f" - - rewrite, err := FollowSymlinkInScope(link, "testdata") - if err != nil { - t.Fatal(err) - } - - if expected := abs(t, "testdata/test"); expected != rewrite { - t.Fatalf("Expected %s got %s", expected, rewrite) - } - - link = "testdata/fs/b/h" - - rewrite, err = FollowSymlinkInScope(link, "testdata") - if err != nil { - t.Fatal(err) - } - - if expected := abs(t, "testdata/root"); expected != rewrite { - t.Fatalf("Expected %s got %s", expected, rewrite) + // avoid letting symlink f lead us out of the "testdata" scope + // we don't normalize because symlink f is in scope and there is no + // information leak + { + link := "testdata/fs/a/f" + + rewrite, err := FollowSymlinkInScope(link, "testdata") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/test"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } + } + + // avoid letting symlink f lead us out of the "testdata/fs" scope + // we don't normalize because symlink f is in scope and there is no + // information leak + { + link := "testdata/fs/a/f" + + rewrite, err := FollowSymlinkInScope(link, "testdata/fs") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/fs/test"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } + } + + // avoid letting symlink g (pointed at by symlink h) take out of scope + // TODO: we should probably normalize to scope here because ../[....]/root + // is out of scope and we leak information + { + link := "testdata/fs/b/h" + + rewrite, err := FollowSymlinkInScope(link, "testdata") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/root"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } + } + + // avoid letting allowing symlink e lead us to ../b + // normalize to the "testdata/fs/a" + { + link := "testdata/fs/a/e" + + rewrite, err := FollowSymlinkInScope(link, "testdata/fs/a") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/fs/a"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } + } + + // avoid letting symlink -> ../directory/file escape from scope + // normalize to "testdata/fs/j" + { + link := "testdata/fs/j/k" + + rewrite, err := FollowSymlinkInScope(link, "testdata/fs/j") + if err != nil { + t.Fatal(err) + } + + if expected := abs(t, "testdata/fs/j"); expected != rewrite { + t.Fatalf("Expected %s got %s", expected, rewrite) + } + } + + // make sure we don't allow escaping to / + // normalize to dir + { + dir, err := ioutil.TempDir("", "docker-fs-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + linkFile := filepath.Join(dir, "foo") + os.Mkdir(filepath.Join(dir, ""), 0700) + os.Symlink("/", linkFile) + + rewrite, err := FollowSymlinkInScope(linkFile, dir) + if err != nil { + t.Fatal(err) + } + + if rewrite != dir { + t.Fatalf("Expected %s got %s", dir, rewrite) + } + } + + // make sure we don't allow escaping to / + // normalize to dir + { + dir, err := ioutil.TempDir("", "docker-fs-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + linkFile := filepath.Join(dir, "foo") + os.Mkdir(filepath.Join(dir, ""), 0700) + os.Symlink("/../../", linkFile) + + rewrite, err := FollowSymlinkInScope(linkFile, dir) + if err != nil { + t.Fatal(err) + } + + if rewrite != dir { + t.Fatalf("Expected %s got %s", dir, rewrite) + } + } + + // make sure we stay in scope without leaking information + // this also checks for escaping to / + // normalize to dir + { + dir, err := ioutil.TempDir("", "docker-fs-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + linkFile := filepath.Join(dir, "foo") + os.Mkdir(filepath.Join(dir, ""), 0700) + os.Symlink("../../", linkFile) + + rewrite, err := FollowSymlinkInScope(linkFile, dir) + if err != nil { + t.Fatal(err) + } + + if rewrite != dir { + t.Fatalf("Expected %s got %s", dir, rewrite) + } } } diff -Nru docker.io-1.3.1~dfsg1/reexec/README.md docker.io-1.3.2~dfsg1/reexec/README.md --- docker.io-1.3.1~dfsg1/reexec/README.md 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/reexec/README.md 1969-12-31 19:00:00.000000000 -0500 @@ -1,5 +0,0 @@ -## reexec - -The `reexec` package facilitates the busybox style reexec of the docker binary that we require because -of the forking limitations of using Go. Handlers can be registered with a name and the argv 0 of -the exec of the binary will be used to find and execute custom init paths. diff -Nru docker.io-1.3.1~dfsg1/reexec/reexec.go docker.io-1.3.2~dfsg1/reexec/reexec.go --- docker.io-1.3.1~dfsg1/reexec/reexec.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/reexec/reexec.go 1969-12-31 19:00:00.000000000 -0500 @@ -1,45 +0,0 @@ -package reexec - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" -) - -var registeredInitializers = make(map[string]func()) - -// Register adds an initialization func under the specified name -func Register(name string, initializer func()) { - if _, exists := registeredInitializers[name]; exists { - panic(fmt.Sprintf("reexec func already registred under name %q", name)) - } - - registeredInitializers[name] = initializer -} - -// Init is called as the first part of the exec process and returns true if an -// initialization function was called. -func Init() bool { - initializer, exists := registeredInitializers[os.Args[0]] - if exists { - initializer() - - return true - } - - return false -} - -// Self returns the path to the current processes binary -func Self() string { - name := os.Args[0] - - if filepath.Base(name) == name { - if lp, err := exec.LookPath(name); err == nil { - name = lp - } - } - - return name -} diff -Nru docker.io-1.3.1~dfsg1/registry/auth.go docker.io-1.3.2~dfsg1/registry/auth.go --- docker.io-1.3.1~dfsg1/registry/auth.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/registry/auth.go 2014-11-24 12:38:01.000000000 -0500 @@ -7,6 +7,7 @@ "fmt" "io/ioutil" "net/http" + "net/url" "os" "path" "strings" @@ -27,8 +28,17 @@ var ( ErrConfigFileMissing = errors.New("The Auth config file is missing") + IndexServerURL *url.URL ) +func init() { + url, err := url.Parse(INDEXSERVER) + if err != nil { + panic(err) + } + IndexServerURL = url +} + type AuthConfig struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` diff -Nru docker.io-1.3.1~dfsg1/registry/endpoint.go docker.io-1.3.2~dfsg1/registry/endpoint.go --- docker.io-1.3.1~dfsg1/registry/endpoint.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/registry/endpoint.go 2014-11-24 12:38:01.000000000 -0500 @@ -4,6 +4,7 @@ "encoding/json" "fmt" "io/ioutil" + "net" "net/http" "net/url" "strings" @@ -11,6 +12,9 @@ "github.com/docker/docker/pkg/log" ) +// for mocking in unit tests +var lookupIP = net.LookupIP + // scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version. func scanForApiVersion(hostname string) (string, APIVersion) { var ( @@ -33,17 +37,8 @@ return hostname, DefaultAPIVersion } -func NewEndpoint(hostname string, secure bool) (*Endpoint, error) { - var ( - endpoint = Endpoint{secure: secure} - trimmedHostname string - err error - ) - if !strings.HasPrefix(hostname, "http") { - hostname = "https://" + hostname - } - trimmedHostname, endpoint.Version = scanForApiVersion(hostname) - endpoint.URL, err = url.Parse(trimmedHostname) +func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) { + endpoint, err := newEndpoint(hostname, insecureRegistries) if err != nil { return nil, err } @@ -54,7 +49,7 @@ //TODO: triggering highland build can be done there without "failing" - if secure { + if endpoint.secure { // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) @@ -65,12 +60,32 @@ endpoint.URL.Scheme = "http" _, err2 := endpoint.Ping() if err2 == nil { - return &endpoint, nil + return endpoint, nil } return nil, fmt.Errorf("Invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) } + return endpoint, nil +} +func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) { + var ( + endpoint = Endpoint{} + trimmedHostname string + err error + ) + if !strings.HasPrefix(hostname, "http") { + hostname = "https://" + hostname + } + trimmedHostname, endpoint.Version = scanForApiVersion(hostname) + endpoint.URL, err = url.Parse(trimmedHostname) + if err != nil { + return nil, err + } + endpoint.secure, err = isSecure(endpoint.URL.Host, insecureRegistries) + if err != nil { + return nil, err + } return &endpoint, nil } @@ -141,18 +156,58 @@ return info, nil } -// IsSecure returns false if the provided hostname is part of the list of insecure registries. +// isSecure returns false if the provided hostname is part of the list of insecure registries. // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. -func IsSecure(hostname string, insecureRegistries []string) bool { - if hostname == IndexServerAddress() { - return true +// +// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. +// If the subnet contains one of the IPs of the registry specified by hostname, the latter is considered +// insecure. +// +// hostname should be a URL.Host (`host:port` or `host`) +func isSecure(hostname string, insecureRegistries []string) (bool, error) { + if hostname == IndexServerURL.Host { + return true, nil + } + + host, _, err := net.SplitHostPort(hostname) + if err != nil { + // assume hostname is of the form `host` without the port and go on. + host = hostname + } + addrs, err := lookupIP(host) + if err != nil { + ip := net.ParseIP(host) + if ip == nil { + // if resolving `host` fails, error out, since host is to be net.Dial-ed anyway + return true, fmt.Errorf("issecure: could not resolve %q: %v", host, err) + } + addrs = []net.IP{ip} + } + if len(addrs) == 0 { + return true, fmt.Errorf("issecure: could not resolve %q", host) } - for _, h := range insecureRegistries { - if hostname == h { - return false + for _, addr := range addrs { + for _, r := range insecureRegistries { + // hostname matches insecure registry + if hostname == r { + return false, nil + } + + // now assume a CIDR was passed to --insecure-registry + _, ipnet, err := net.ParseCIDR(r) + if err != nil { + // if could not parse it as a CIDR, even after removing + // assume it's not a CIDR and go on with the next candidate + continue + } + + // check if the addr falls in the subnet + if ipnet.Contains(addr) { + return false, nil + } } } - return true + return true, nil } diff -Nru docker.io-1.3.1~dfsg1/registry/endpoint_test.go docker.io-1.3.2~dfsg1/registry/endpoint_test.go --- docker.io-1.3.1~dfsg1/registry/endpoint_test.go 1969-12-31 19:00:00.000000000 -0500 +++ docker.io-1.3.2~dfsg1/registry/endpoint_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -0,0 +1,27 @@ +package registry + +import "testing" + +func TestEndpointParse(t *testing.T) { + testData := []struct { + str string + expected string + }{ + {IndexServerAddress(), IndexServerAddress()}, + {"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"}, + {"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"}, + } + for _, td := range testData { + e, err := newEndpoint(td.str, insecureRegistries) + if err != nil { + t.Errorf("%q: %s", td.str, err) + } + if e == nil { + t.Logf("something's fishy, endpoint for %q is nil", td.str) + continue + } + if e.String() != td.expected { + t.Errorf("expected %q, got %q", td.expected, e.String()) + } + } +} diff -Nru docker.io-1.3.1~dfsg1/registry/registry_mock_test.go docker.io-1.3.2~dfsg1/registry/registry_mock_test.go --- docker.io-1.3.1~dfsg1/registry/registry_mock_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/registry/registry_mock_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -2,9 +2,11 @@ import ( "encoding/json" + "errors" "fmt" "io" "io/ioutil" + "net" "net/http" "net/http/httptest" "net/url" @@ -19,8 +21,9 @@ ) var ( - testHttpServer *httptest.Server - testLayers = map[string]map[string]string{ + testHTTPServer *httptest.Server + insecureRegistries []string + testLayers = map[string]map[string]string{ "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": { "json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", "comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00", @@ -79,6 +82,11 @@ "latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", }, } + mockHosts = map[string][]net.IP{ + "": {net.ParseIP("0.0.0.0")}, + "localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + "example.com": {net.ParseIP("42.42.42.42")}, + } ) func init() { @@ -99,7 +107,31 @@ // /v2/ r.HandleFunc("/v2/version", handlerGetPing).Methods("GET") - testHttpServer = httptest.NewServer(handlerAccessLog(r)) + testHTTPServer = httptest.NewServer(handlerAccessLog(r)) + URL, err := url.Parse(testHTTPServer.URL) + if err != nil { + panic(err) + } + insecureRegistries = []string{URL.Host} + + // override net.LookupIP + lookupIP = func(host string) ([]net.IP, error) { + if host == "127.0.0.1" { + // I believe in future Go versions this will fail, so let's fix it later + return net.LookupIP(host) + } + for h, addrs := range mockHosts { + if host == h { + return addrs, nil + } + for _, addr := range addrs { + if addr.String() == host { + return []net.IP{addr}, nil + } + } + } + return nil, errors.New("lookup: no such host") + } } func handlerAccessLog(handler http.Handler) http.Handler { @@ -111,7 +143,7 @@ } func makeURL(req string) string { - return testHttpServer.URL + req + return testHTTPServer.URL + req } func writeHeaders(w http.ResponseWriter) { @@ -301,7 +333,7 @@ } func handlerImages(w http.ResponseWriter, r *http.Request) { - u, _ := url.Parse(testHttpServer.URL) + u, _ := url.Parse(testHTTPServer.URL) w.Header().Add("X-Docker-Endpoints", fmt.Sprintf("%s , %s ", u.Host, "test.example.com")) w.Header().Add("X-Docker-Token", fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())) if r.Method == "PUT" { diff -Nru docker.io-1.3.1~dfsg1/registry/registry_test.go docker.io-1.3.2~dfsg1/registry/registry_test.go --- docker.io-1.3.1~dfsg1/registry/registry_test.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/registry/registry_test.go 2014-11-24 12:38:01.000000000 -0500 @@ -18,7 +18,7 @@ func spawnTestRegistrySession(t *testing.T) *Session { authConfig := &AuthConfig{} - endpoint, err := NewEndpoint(makeURL("/v1/"), false) + endpoint, err := NewEndpoint(makeURL("/v1/"), insecureRegistries) if err != nil { t.Fatal(err) } @@ -30,7 +30,7 @@ } func TestPingRegistryEndpoint(t *testing.T) { - ep, err := NewEndpoint(makeURL("/v1/"), false) + ep, err := NewEndpoint(makeURL("/v1/"), insecureRegistries) if err != nil { t.Fatal(err) } @@ -316,3 +316,40 @@ } } } + +func TestIsSecure(t *testing.T) { + tests := []struct { + addr string + insecureRegistries []string + expected bool + }{ + {IndexServerURL.Host, nil, true}, + {"example.com", []string{}, true}, + {"example.com", []string{"example.com"}, false}, + {"localhost", []string{"localhost:5000"}, false}, + {"localhost:5000", []string{"localhost:5000"}, false}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false}, + {"localhost", nil, false}, + {"localhost:5000", nil, false}, + {"127.0.0.1", nil, false}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1", []string{"example.com"}, false}, + {"example.com", nil, true}, + {"example.com", []string{"example.com"}, false}, + {"127.0.0.1", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"example.com"}, false}, + {"example.com:5000", []string{"42.42.0.0/16"}, false}, + {"example.com", []string{"42.42.0.0/16"}, false}, + {"example.com:5000", []string{"42.42.42.42/8"}, false}, + {"127.0.0.1:5000", []string{"127.0.0.0/8"}, false}, + {"42.42.42.42:5000", []string{"42.1.1.1/8"}, false}, + } + for _, tt := range tests { + // TODO: remove this once we remove localhost insecure by default + insecureRegistries := append(tt.insecureRegistries, "127.0.0.0/8") + if sec, err := isSecure(tt.addr, insecureRegistries); err != nil || sec != tt.expected { + t.Fatalf("isSecure failed for %q %v, expected %v got %v. Error: %v", tt.addr, insecureRegistries, tt.expected, sec, err) + } + } +} diff -Nru docker.io-1.3.1~dfsg1/registry/service.go docker.io-1.3.2~dfsg1/registry/service.go --- docker.io-1.3.1~dfsg1/registry/service.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/registry/service.go 2014-11-24 12:38:01.000000000 -0500 @@ -40,7 +40,7 @@ job.GetenvJson("authConfig", authConfig) if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() { - endpoint, err := NewEndpoint(addr, IsSecure(addr, s.insecureRegistries)) + endpoint, err := NewEndpoint(addr, s.insecureRegistries) if err != nil { return job.Error(err) } @@ -92,9 +92,7 @@ return job.Error(err) } - secure := IsSecure(hostname, s.insecureRegistries) - - endpoint, err := NewEndpoint(hostname, secure) + endpoint, err := NewEndpoint(hostname, s.insecureRegistries) if err != nil { return job.Error(err) } diff -Nru docker.io-1.3.1~dfsg1/runconfig/config.go docker.io-1.3.2~dfsg1/runconfig/config.go --- docker.io-1.3.1~dfsg1/runconfig/config.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/runconfig/config.go 2014-11-24 12:38:01.000000000 -0500 @@ -32,7 +32,6 @@ Entrypoint []string NetworkDisabled bool OnBuild []string - SecurityOpt []string } func ContainerConfigFromJob(job *engine.Job) *Config { @@ -56,7 +55,6 @@ } job.GetenvJson("ExposedPorts", &config.ExposedPorts) job.GetenvJson("Volumes", &config.Volumes) - config.SecurityOpt = job.GetenvList("SecurityOpt") if PortSpecs := job.GetenvList("PortSpecs"); PortSpecs != nil { config.PortSpecs = PortSpecs } diff -Nru docker.io-1.3.1~dfsg1/runconfig/hostconfig.go docker.io-1.3.2~dfsg1/runconfig/hostconfig.go --- docker.io-1.3.1~dfsg1/runconfig/hostconfig.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/runconfig/hostconfig.go 2014-11-24 12:38:01.000000000 -0500 @@ -56,6 +56,7 @@ CapAdd []string CapDrop []string RestartPolicy RestartPolicy + SecurityOpt []string } // This is used by the create command when you want to set both the @@ -90,6 +91,7 @@ job.GetenvJson("PortBindings", &hostConfig.PortBindings) job.GetenvJson("Devices", &hostConfig.Devices) job.GetenvJson("RestartPolicy", &hostConfig.RestartPolicy) + hostConfig.SecurityOpt = job.GetenvList("SecurityOpt") if Binds := job.GetenvList("Binds"); Binds != nil { hostConfig.Binds = Binds } diff -Nru docker.io-1.3.1~dfsg1/runconfig/parse.go docker.io-1.3.2~dfsg1/runconfig/parse.go --- docker.io-1.3.1~dfsg1/runconfig/parse.go 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/runconfig/parse.go 2014-11-24 12:38:01.000000000 -0500 @@ -256,7 +256,6 @@ Volumes: flVolumes.GetMap(), Entrypoint: entrypoint, WorkingDir: *flWorkingDir, - SecurityOpt: flSecurityOpt.GetAll(), } hostConfig := &HostConfig{ @@ -276,6 +275,7 @@ CapAdd: flCapAdd.GetAll(), CapDrop: flCapDrop.GetAll(), RestartPolicy: restartPolicy, + SecurityOpt: flSecurityOpt.GetAll(), } if sysInfo != nil && flMemory > 0 && !sysInfo.SwapLimit { diff -Nru docker.io-1.3.1~dfsg1/VERSION docker.io-1.3.2~dfsg1/VERSION --- docker.io-1.3.1~dfsg1/VERSION 2014-10-30 09:44:46.000000000 -0400 +++ docker.io-1.3.2~dfsg1/VERSION 2014-11-24 12:38:01.000000000 -0500 @@ -1 +1 @@ -1.3.1 +1.3.2
signature.asc
Description: Digital signature