diff --git a/container.go b/container.go index 118ce1b034..9dac345447 100644 --- a/container.go +++ b/container.go @@ -894,7 +894,13 @@ func (container *Container) Start() (err error) { return err } + var lxcStart string = "lxc-start" + if container.hostConfig.Privileged && container.runtime.capabilities.AppArmor { + lxcStart = path.Join(container.runtime.config.Root, "lxc-start-unconfined") + } + params := []string{ + lxcStart, "-n", container.ID, "-f", container.lxcConfigPath(), "--", @@ -987,11 +993,24 @@ func (container *Container) Start() (err error) { params = append(params, "--", container.Path) params = append(params, container.Args...) - var lxcStart string = "lxc-start" - if container.hostConfig.Privileged && container.runtime.capabilities.AppArmor { - lxcStart = path.Join(container.runtime.config.Root, "lxc-start-unconfined") + if RootIsShared() { + // lxc-start really needs / to be non-shared, or all kinds of stuff break + // when lxc-start unmount things and those unmounts propagate to the main + // mount namespace. + // What we really want is to clone into a new namespace and then + // mount / MS_REC|MS_SLAVE, but since we can't really clone or fork + // without exec in go we have to do this horrible shell hack... + shellString := + "mount --make-rslave /; exec " + + utils.ShellQuoteArguments(params) + + params = []string{ + "unshare", "-m", "--", "/bin/sh", "-c", shellString, + } } - container.cmd = exec.Command(lxcStart, params...) + + container.cmd = exec.Command(params[0], params[1:]...) + // Setup logging of stdout and stderr to disk if err := container.runtime.LogToDisk(container.stdout, container.logPath("json"), "stdout"); err != nil { return err diff --git a/utils.go b/utils.go index 1393a5c0a8..82b163c608 100644 --- a/utils.go +++ b/utils.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/dotcloud/docker/namesgenerator" "github.com/dotcloud/docker/utils" + "io/ioutil" "strconv" "strings" ) @@ -310,6 +311,20 @@ func parseLink(rawLink string) (map[string]string, error) { return utils.PartParser("name:alias", rawLink) } +func RootIsShared() bool { + if data, err := ioutil.ReadFile("/proc/self/mountinfo"); err == nil { + for _, line := range strings.Split(string(data), "\n") { + cols := strings.Split(line, " ") + if len(cols) >= 6 && cols[4] == "/" { + return strings.HasPrefix(cols[6], "shared") + } + } + } + + // No idea, probably safe to assume so + return true +} + type checker struct { runtime *Runtime } diff --git a/utils/utils.go b/utils/utils.go index fe75aa68dc..d16ffe3171 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1179,6 +1179,41 @@ func (e *StatusError) Error() string { return fmt.Sprintf("Status: %d", e.Status) } +func quote(word string, buf *bytes.Buffer) { + // Bail out early for "simple" strings + if word != "" && !strings.ContainsAny(word, "\\'\"`${[|&;<>()~*?! \t\n") { + buf.WriteString(word) + return + } + + buf.WriteString("'") + + for i := 0; i < len(word); i++ { + b := word[i] + if b == '\'' { + // Replace literal ' with a close ', a \', and a open ' + buf.WriteString("'\\''") + } else { + buf.WriteByte(b) + } + } + + buf.WriteString("'") +} + +// Take a list of strings and escape them so they will be handled right +// when passed as arguments to an program via a shell +func ShellQuoteArguments(args []string) string { + var buf bytes.Buffer + for i, arg := range args { + if i != 0 { + buf.WriteByte(' ') + } + quote(arg, &buf) + } + return buf.String() +} + func IsClosedError(err error) bool { /* This comparison is ugly, but unfortunately, net.go doesn't export errClosing. * See: