rootless: support `--ipc=host`



<!--
Please make sure you've read and understood our contributing guidelines;
https://github.com/moby/moby/blob/master/CONTRIBUTING.md

** Make sure all your commits include a signature generated with `git commit -s` **

For additional information on our contributing process, read our contributing
guide https://docs.docker.com/opensource/code/

If this is a bug fix, make sure your description includes "fixes #xxxx", or
"closes #xxxx"

Please provide the following information:
-->

**- What I did**

rootless: support `--ipc=host`

Fixes #44294

**- How I did it**

By bind-mounting `/dev/{shm,mqueue}` for rootless.

**- How to verify it**

```console
$ touch /dev/shm/foo
$ docker --context=rootless run --rm --ipc=host alpine ls /dev/shm
foo
```

Previously it was failing with:
```
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "mqueue" to rootfs at "/dev/mqueue": mount mqueue:/dev/mqueue (via /proc/self/fd/7), flags: 0xe: operation not permitted: unknown.
```

**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->
rootless: support `--ipc=host`

**- A picture of a cute animal (not mandatory but encouraged)**

🐧 
pkg/rootless/specconv/specconv_linux.go
package specconv // import "github.com/docker/docker/pkg/rootless/specconv"

import (
	"os"
	"path"
	"strconv"
	"strings"

	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
)

// ToRootless converts spec to be compatible with "rootless" runc.
// * Remove non-supported cgroups
// * Fix up OOMScoreAdj
// * Fix up /proc if --pid=host
//
// v2Controllers should be non-nil only if running with v2 and systemd.
func ToRootless(spec *specs.Spec, v2Controllers []string) error {
	return toRootless(spec, v2Controllers, getCurrentOOMScoreAdj())
}

func getCurrentOOMScoreAdj() int {
	b, err := os.ReadFile("/proc/self/oom_score_adj")
	if err != nil {
		logrus.WithError(err).Warn("failed to read /proc/self/oom_score_adj")
		return 0
	}
	s := string(b)
	i, err := strconv.Atoi(strings.TrimSpace(s))
	if err != nil {
		logrus.WithError(err).Warnf("failed to parse /proc/self/oom_score_adj (%q)", s)
		return 0
	}
	return i
}

func toRootless(spec *specs.Spec, v2Controllers []string, currentOOMScoreAdj int) error {
	if len(v2Controllers) == 0 {
		// Remove cgroup settings.
		spec.Linux.Resources = nil
		spec.Linux.CgroupsPath = ""
	} else {
		if spec.Linux.Resources != nil {
			m := make(map[string]struct{})
			for _, s := range v2Controllers {
				m[s] = struct{}{}
			}
			// Remove devices: https://github.com/containers/crun/issues/255
			spec.Linux.Resources.Devices = nil
			if _, ok := m["memory"]; !ok {
				spec.Linux.Resources.Memory = nil
			}
			if _, ok := m["cpu"]; !ok {
				spec.Linux.Resources.CPU = nil
			}
			if _, ok := m["cpuset"]; !ok {
				if spec.Linux.Resources.CPU != nil {
					spec.Linux.Resources.CPU.Cpus = ""
					spec.Linux.Resources.CPU.Mems = ""
				}
			}
			if _, ok := m["pids"]; !ok {
				spec.Linux.Resources.Pids = nil
			}
			if _, ok := m["io"]; !ok {
				spec.Linux.Resources.BlockIO = nil
			}
			if _, ok := m["rdma"]; !ok {
				spec.Linux.Resources.Rdma = nil
			}
			spec.Linux.Resources.HugepageLimits = nil
			spec.Linux.Resources.Network = nil
		}
	}

	if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
		*spec.Process.OOMScoreAdj = currentOOMScoreAdj
	}

	// Fix up /proc if --pid=host
	pidHost, err := isPidHost(spec)
	if err != nil {
		return err
	}
	if !pidHost {
		return nil
	}
	return bindMountHostProcfs(spec)
}

func isPidHost(spec *specs.Spec) (bool, error) {
	for _, ns := range spec.Linux.Namespaces {
		if ns.Type == specs.PIDNamespace {
			if ns.Path == "" {
				return false, nil
			}
			pidNS, err := os.Readlink(ns.Path)
			if err != nil {
				return false, err
			}
			selfPidNS, err := os.Readlink("/proc/self/ns/pid")
			if err != nil {
				return false, err
			}
			return pidNS == selfPidNS, nil
		}
	}
	return true, nil
}

func bindMountHostProcfs(spec *specs.Spec) error {
	// Replace procfs mount with rbind
	// https://github.com/containers/podman/blob/v3.0.0-rc1/pkg/specgen/generate/oci.go#L248-L257
	for i, m := range spec.Mounts {
		if path.Clean(m.Destination) == "/proc" {
			newM := specs.Mount{
				Destination: "/proc",
				Type:        "bind",
				Source:      "/proc",
				Options:     []string{"rbind", "nosuid", "noexec", "nodev"},
			}
			spec.Mounts[i] = newM
		}
	}

	// Remove ReadonlyPaths for /proc/*
	newROP := spec.Linux.ReadonlyPaths[:0]
	for _, s := range spec.Linux.ReadonlyPaths {
		s = path.Clean(s)
		if !strings.HasPrefix(s, "/proc/") {
			newROP = append(newROP, s)
		}
	}
	spec.Linux.ReadonlyPaths = newROP

	return nil
}

package specconv // import "github.com/docker/docker/pkg/rootless/specconv"

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"

	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
)

// ToRootless converts spec to be compatible with "rootless" runc.
// * Remove non-supported cgroups
// * Fix up OOMScoreAdj
// * Fix up /proc if --pid=host
// * Fix up /dev/shm and /dev/mqueue if --ipc=host
//
// v2Controllers should be non-nil only if running with v2 and systemd.
func ToRootless(spec *specs.Spec, v2Controllers []string) error {
	return toRootless(spec, v2Controllers, getCurrentOOMScoreAdj())
}

func getCurrentOOMScoreAdj() int {
	b, err := os.ReadFile("/proc/self/oom_score_adj")
	if err != nil {
		logrus.WithError(err).Warn("failed to read /proc/self/oom_score_adj")
		return 0
	}
	s := string(b)
	i, err := strconv.Atoi(strings.TrimSpace(s))
	if err != nil {
		logrus.WithError(err).Warnf("failed to parse /proc/self/oom_score_adj (%q)", s)
		return 0
	}
	return i
}

func toRootless(spec *specs.Spec, v2Controllers []string, currentOOMScoreAdj int) error {
	if len(v2Controllers) == 0 {
		// Remove cgroup settings.
		spec.Linux.Resources = nil
		spec.Linux.CgroupsPath = ""
	} else {
		if spec.Linux.Resources != nil {
			m := make(map[string]struct{})
			for _, s := range v2Controllers {
				m[s] = struct{}{}
			}
			// Remove devices: https://github.com/containers/crun/issues/255
			spec.Linux.Resources.Devices = nil
			if _, ok := m["memory"]; !ok {
				spec.Linux.Resources.Memory = nil
			}
			if _, ok := m["cpu"]; !ok {
				spec.Linux.Resources.CPU = nil
			}
			if _, ok := m["cpuset"]; !ok {
				if spec.Linux.Resources.CPU != nil {
					spec.Linux.Resources.CPU.Cpus = ""
					spec.Linux.Resources.CPU.Mems = ""
				}
			}
			if _, ok := m["pids"]; !ok {
				spec.Linux.Resources.Pids = nil
			}
			if _, ok := m["io"]; !ok {
				spec.Linux.Resources.BlockIO = nil
			}
			if _, ok := m["rdma"]; !ok {
				spec.Linux.Resources.Rdma = nil
			}
			spec.Linux.Resources.HugepageLimits = nil
			spec.Linux.Resources.Network = nil
		}
	}

	if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
		*spec.Process.OOMScoreAdj = currentOOMScoreAdj
	}

	// Fix up /proc if --pid=host
	pidHost, err := isHostNS(spec, specs.PIDNamespace)
	if err != nil {
		return err
	}
	if pidHost {
		if err := bindMountHostProcfs(spec); err != nil {
			return err
		}
	}

	// Fix up /dev/shm and /dev/mqueue if --ipc=host
	ipcHost, err := isHostNS(spec, specs.IPCNamespace)
	if err != nil {
		return err
	}
	if ipcHost {
		if err := bindMountHostIPC(spec); err != nil {
			return err
		}
	}

	return nil
}

func isHostNS(spec *specs.Spec, nsType specs.LinuxNamespaceType) (bool, error) {
	if strings.Contains(string(nsType), string(os.PathSeparator)) {
		return false, fmt.Errorf("unexpected namespace type %q", nsType)
	}
	for _, ns := range spec.Linux.Namespaces {
		if ns.Type == nsType {
			if ns.Path == "" {
				return false, nil
			}
			ns, err := os.Readlink(ns.Path)
			if err != nil {
				return false, err
			}
			selfNS, err := os.Readlink(filepath.Join("/proc/self/ns", string(nsType)))
			if err != nil {
				return false, err
			}
			return ns == selfNS, nil
		}
	}
	return true, nil
}

func bindMountHostProcfs(spec *specs.Spec) error {
	// Replace procfs mount with rbind
	// https://github.com/containers/podman/blob/v3.0.0-rc1/pkg/specgen/generate/oci.go#L248-L257
	for i, m := range spec.Mounts {
		if path.Clean(m.Destination) == "/proc" {
			newM := specs.Mount{
				Destination: "/proc",
				Type:        "bind",
				Source:      "/proc",
				Options:     []string{"rbind", "nosuid", "noexec", "nodev"},
			}
			spec.Mounts[i] = newM
		}
	}

	// Remove ReadonlyPaths for /proc/*
	newROP := spec.Linux.ReadonlyPaths[:0]
	for _, s := range spec.Linux.ReadonlyPaths {
		s = path.Clean(s)
		if !strings.HasPrefix(s, "/proc/") {
			newROP = append(newROP, s)
		}
	}
	spec.Linux.ReadonlyPaths = newROP

	return nil
}

// withBindMountHostIPC replaces /dev/shm and /dev/mqueue mount with rbind.
// Required for --ipc=host on rootless.
//
// Based on https://github.com/containerd/nerdctl/blob/v1.1.0/cmd/nerdctl/run.go#L836-L860
func bindMountHostIPC(s *specs.Spec) error {
	for i, m := range s.Mounts {
		switch p := path.Clean(m.Destination); p {
		case "/dev/shm", "/dev/mqueue":
			s.Mounts[i] = specs.Mount{
				Destination: p,
				Type:        "bind",
				Source:      p,
				Options:     []string{"rbind", "nosuid", "noexec", "nodev"},
			}
		}
	}
	return nil
}

> That doesn't seem to work

Interesting. Perhaps I made a mistake, or something else is missing in that case. Looks like a fun thing to look into, to see what differs (what option is missing).

Thanks for trying it!