diff --git a/CHANGELOG.md b/CHANGELOG.md index d168ad280a..eda6497892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 1.6.1 (2015-05-07) + +#### Security +- Fix read/write /proc paths (CVE-2015-3630) +- Prohibit VOLUME /proc and VOLUME / (CVE-2015-3631) +- Fix opening of file-descriptor 1 (CVE-2015-3627) +- Fix symlink traversal on container respawn allowing local privilege escalation (CVE-2015-3629) +- Prohibit mount of /sys + +#### Runtime +- Update Apparmor policy to not allow mounts + ## 1.6.0 (2015-04-07) #### Builder diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index fbad9f1301..ffec5ec6c3 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -6,12 +6,10 @@ import ( "errors" "fmt" "net" - "path/filepath" "strings" "syscall" "github.com/docker/docker/daemon/execdriver" - "github.com/docker/docker/pkg/symlink" "github.com/docker/libcontainer/apparmor" "github.com/docker/libcontainer/configs" "github.com/docker/libcontainer/devices" @@ -231,10 +229,6 @@ func (d *driver) setupMounts(container *configs.Config, c *execdriver.Command) e container.Mounts = defaultMounts for _, m := range c.Mounts { - dest, err := symlink.FollowSymlinkInScope(filepath.Join(c.Rootfs, m.Destination), c.Rootfs) - if err != nil { - return err - } flags := syscall.MS_BIND | syscall.MS_REC if !m.Writable { flags |= syscall.MS_RDONLY @@ -242,10 +236,9 @@ func (d *driver) setupMounts(container *configs.Config, c *execdriver.Command) e if m.Slave { flags |= syscall.MS_SLAVE } - container.Mounts = append(container.Mounts, &configs.Mount{ Source: m.Source, - Destination: dest, + Destination: m.Destination, Device: "bind", Flags: flags, }) diff --git a/daemon/execdriver/native/template/default_template.go b/daemon/execdriver/native/template/default_template.go index 76e3cea787..ecedcfc8cb 100644 --- a/daemon/execdriver/native/template/default_template.go +++ b/daemon/execdriver/native/template/default_template.go @@ -82,9 +82,16 @@ func New() *configs.Config { }, MaskPaths: []string{ "/proc/kcore", + "/proc/latency_stats", + "/proc/timer_stats", }, ReadonlyPaths: []string{ - "/proc/sys", "/proc/sysrq-trigger", "/proc/irq", "/proc/bus", + "/proc/asound", + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger", }, } diff --git a/hack/vendor.sh b/hack/vendor.sh index 68d04f544a..d1c098f692 100755 --- a/hack/vendor.sh +++ b/hack/vendor.sh @@ -67,7 +67,7 @@ mv tmp-digest src/github.com/docker/distribution/digest mkdir -p src/github.com/docker/distribution/registry mv tmp-api src/github.com/docker/distribution/registry/api -clone git github.com/docker/libcontainer 6607689b1d06743003a45a722d9fe0bef36b274e +clone git github.com/docker/libcontainer 1b471834b45063b61e0aedefbb1739a8f34b414e # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file) rm -rf src/github.com/docker/libcontainer/vendor eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')" diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 626cc3583a..8849a2d1b1 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -3056,3 +3056,65 @@ func (s *DockerSuite) TestRunPidHostWithChildIsKillable(c *check.C) { c.Fatal("Kill container timed out") } } + +func (s *DockerSuite) TestRunWithTooSmallMemoryLimit(c *check.C) { + defer deleteAllContainers() + // this memory limit is 1 byte less than the min, which is 4MB + // https://github.com/docker/docker/blob/v1.5.0/daemon/create.go#L22 + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-m", "4194303", "busybox")) + if err == nil || !strings.Contains(out, "Minimum memory limit allowed is 4MB") { + c.Fatalf("expected run to fail when using too low a memory limit: %q", out) + } +} + +func (s *DockerSuite) TestRunWriteToProcAsound(c *check.C) { + defer deleteAllContainers() + code, err := runCommand(exec.Command(dockerBinary, "run", "busybox", "sh", "-c", "echo 111 >> /proc/asound/version")) + if err == nil || code == 0 { + c.Fatal("standard container should not be able to write to /proc/asound") + } +} + +func (s *DockerSuite) TestRunReadProcTimer(c *check.C) { + defer deleteAllContainers() + out, code, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "busybox", "cat", "/proc/timer_stats")) + if err != nil || code != 0 { + c.Fatal(err) + } + if strings.Trim(out, "\n ") != "" { + c.Fatalf("expected to receive no output from /proc/timer_stats but received %q", out) + } +} + +func (s *DockerSuite) TestRunReadProcLatency(c *check.C) { + // some kernels don't have this configured so skip the test if this file is not found + // on the host running the tests. + if _, err := os.Stat("/proc/latency_stats"); err != nil { + c.Skip("kernel doesnt have latency_stats configured") + return + } + defer deleteAllContainers() + out, code, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "busybox", "cat", "/proc/latency_stats")) + if err != nil || code != 0 { + c.Fatal(err) + } + if strings.Trim(out, "\n ") != "" { + c.Fatalf("expected to receive no output from /proc/latency_stats but received %q", out) + } +} + +func (s *DockerSuite) TestMountIntoProc(c *check.C) { + defer deleteAllContainers() + code, err := runCommand(exec.Command(dockerBinary, "run", "-v", "/proc//sys", "busybox", "true")) + if err == nil || code == 0 { + c.Fatal("container should not be able to mount into /proc") + } +} + +func (s *DockerSuite) TestMountIntoSys(c *check.C) { + defer deleteAllContainers() + code, err := runCommand(exec.Command(dockerBinary, "run", "-v", "/sys/", "busybox", "true")) + if err == nil || code == 0 { + c.Fatal("container should not be able to mount into /sys") + } +} diff --git a/vendor/src/github.com/docker/libcontainer/SPEC.md b/vendor/src/github.com/docker/libcontainer/SPEC.md index d83d758ddd..3ca90d6cd2 100644 --- a/vendor/src/github.com/docker/libcontainer/SPEC.md +++ b/vendor/src/github.com/docker/libcontainer/SPEC.md @@ -217,17 +217,6 @@ profile flags=(attach_disconnected,mediate_deleted) { file, umount, - mount fstype=tmpfs, - mount fstype=mqueue, - mount fstype=fuse.*, - mount fstype=binfmt_misc -> /proc/sys/fs/binfmt_misc/, - mount fstype=efivarfs -> /sys/firmware/efi/efivars/, - mount fstype=fusectl -> /sys/fs/fuse/connections/, - mount fstype=securityfs -> /sys/kernel/security/, - mount fstype=debugfs -> /sys/kernel/debug/, - mount fstype=proc -> /proc/, - mount fstype=sysfs -> /sys/, - deny @{PROC}/sys/fs/** wklx, deny @{PROC}/sysrq-trigger rwklx, deny @{PROC}/mem rwklx, @@ -235,9 +224,7 @@ profile flags=(attach_disconnected,mediate_deleted) { deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx, deny @{PROC}/sys/kernel/*/** wklx, - deny mount options=(ro, remount) -> /, - deny mount fstype=debugfs -> /var/lib/ureadahead/debugfs/, - deny mount fstype=devpts, + deny mount, deny /sys/[^f]*/** wklx, deny /sys/f[^s]*/** wklx, diff --git a/vendor/src/github.com/docker/libcontainer/apparmor/gen.go b/vendor/src/github.com/docker/libcontainer/apparmor/gen.go index 4565f6dfec..a3192e23b2 100644 --- a/vendor/src/github.com/docker/libcontainer/apparmor/gen.go +++ b/vendor/src/github.com/docker/libcontainer/apparmor/gen.go @@ -27,17 +27,6 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) { file, umount, - mount fstype=tmpfs, - mount fstype=mqueue, - mount fstype=fuse.*, - mount fstype=binfmt_misc -> /proc/sys/fs/binfmt_misc/, - mount fstype=efivarfs -> /sys/firmware/efi/efivars/, - mount fstype=fusectl -> /sys/fs/fuse/connections/, - mount fstype=securityfs -> /sys/kernel/security/, - mount fstype=debugfs -> /sys/kernel/debug/, - mount fstype=proc -> /proc/, - mount fstype=sysfs -> /sys/, - deny @{PROC}/sys/fs/** wklx, deny @{PROC}/sysrq-trigger rwklx, deny @{PROC}/mem rwklx, @@ -45,9 +34,7 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) { deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx, deny @{PROC}/sys/kernel/*/** wklx, - deny mount options=(ro, remount) -> /, - deny mount fstype=debugfs -> /var/lib/ureadahead/debugfs/, - deny mount fstype=devpts, + deny mount, deny /sys/[^f]*/** wklx, deny /sys/f[^s]*/** wklx, diff --git a/vendor/src/github.com/docker/libcontainer/rootfs_linux.go b/vendor/src/github.com/docker/libcontainer/rootfs_linux.go index d8c61e97a0..0cd60373c8 100644 --- a/vendor/src/github.com/docker/libcontainer/rootfs_linux.go +++ b/vendor/src/github.com/docker/libcontainer/rootfs_linux.go @@ -13,6 +13,7 @@ import ( "syscall" "time" + "github.com/docker/docker/pkg/symlink" "github.com/docker/libcontainer/cgroups" "github.com/docker/libcontainer/configs" "github.com/docker/libcontainer/label" @@ -48,11 +49,6 @@ func setupRootfs(config *configs.Config, console *linuxConsole) (err error) { if err := setupPtmx(config, console); err != nil { return newSystemError(err) } - // stdin, stdout and stderr could be pointing to /dev/null from parent namespace. - // re-open them inside this namespace. - if err := reOpenDevNull(config.Rootfs); err != nil { - return newSystemError(err) - } if err := setupDevSymlinks(config.Rootfs); err != nil { return newSystemError(err) } @@ -67,6 +63,9 @@ func setupRootfs(config *configs.Config, console *linuxConsole) (err error) { if err != nil { return newSystemError(err) } + if err := reOpenDevNull(config.Rootfs); err != nil { + return newSystemError(err) + } if config.Readonlyfs { if err := setReadonly(); err != nil { return newSystemError(err) @@ -139,6 +138,16 @@ func mountToRootfs(m *configs.Mount, rootfs, mountLabel string) error { // unable to bind anything to it. return err } + // ensure that the destination of the bind mount is resolved of symlinks at mount time because + // any previous mounts can invalidate the next mount's destination. + // this can happen when a user specifies mounts within other mounts to cause breakouts or other + // evil stuff to try to escape the container's rootfs. + if dest, err = symlink.FollowSymlinkInScope(filepath.Join(rootfs, m.Destination), rootfs); err != nil { + return err + } + if err := checkMountDestination(rootfs, dest); err != nil { + return err + } if err := createIfNotExists(dest, stat.IsDir()); err != nil { return err } @@ -197,6 +206,29 @@ func mountToRootfs(m *configs.Mount, rootfs, mountLabel string) error { return nil } +// checkMountDestination checks to ensure that the mount destination is not over the +// top of /proc or /sys. +// dest is required to be an abs path and have any symlinks resolved before calling this function. +func checkMountDestination(rootfs, dest string) error { + if filepath.Clean(rootfs) == filepath.Clean(dest) { + return fmt.Errorf("mounting into / is prohibited") + } + invalidDestinations := []string{ + "/proc", + "/sys", + } + for _, invalid := range invalidDestinations { + path, err := filepath.Rel(filepath.Join(rootfs, invalid), dest) + if err != nil { + return err + } + if path == "." || !strings.HasPrefix(path, "..") { + return fmt.Errorf("%q cannot be mounted because it is located inside %q", dest, invalid) + } + } + return nil +} + func setupDevSymlinks(rootfs string) error { var links = [][2]string{ {"/proc/self/fd", "/dev/fd"}, @@ -221,11 +253,13 @@ func setupDevSymlinks(rootfs string) error { return nil } -// If stdin, stdout or stderr are pointing to '/dev/null' in the global mount namespace, -// this method will make them point to '/dev/null' in this namespace. +// If stdin, stdout, and/or stderr are pointing to `/dev/null` in the parent's rootfs +// this method will make them point to `/dev/null` in this container's rootfs. This +// needs to be called after we chroot/pivot into the container's rootfs so that any +// symlinks are resolved locally. func reOpenDevNull(rootfs string) error { var stat, devNullStat syscall.Stat_t - file, err := os.Open(filepath.Join(rootfs, "/dev/null")) + file, err := os.Open("/dev/null") if err != nil { return fmt.Errorf("Failed to open /dev/null - %s", err) } diff --git a/vendor/src/github.com/docker/libcontainer/rootfs_linux_test.go b/vendor/src/github.com/docker/libcontainer/rootfs_linux_test.go new file mode 100644 index 0000000000..54df065ccc --- /dev/null +++ b/vendor/src/github.com/docker/libcontainer/rootfs_linux_test.go @@ -0,0 +1,37 @@ +// +build linux + +package libcontainer + +import "testing" + +func TestCheckMountDestOnProc(t *testing.T) { + dest := "/rootfs/proc/" + err := checkMountDestination("/rootfs", dest) + if err == nil { + t.Fatal("destination inside proc should return an error") + } +} + +func TestCheckMountDestInSys(t *testing.T) { + dest := "/rootfs//sys/fs/cgroup" + err := checkMountDestination("/rootfs", dest) + if err == nil { + t.Fatal("destination inside proc should return an error") + } +} + +func TestCheckMountDestFalsePositive(t *testing.T) { + dest := "/rootfs/sysfiles/fs/cgroup" + err := checkMountDestination("/rootfs", dest) + if err != nil { + t.Fatal(err) + } +} + +func TestCheckMountRoot(t *testing.T) { + dest := "/rootfs" + err := checkMountDestination("/rootfs", dest) + if err == nil { + t.Fatal(err) + } +}