Repository: moby/moby (language: go)

Issue:
Support for IPC namespace in rootless docker
### Description

I ran a busybox container with `--ipc=host` and it failed on a SLES 15 vm with rootless docker installed.
```
# pid=host
foo@linux:~> docker run -d --pid=host busybox sleep 1234
7f81333bac62d6a01ec106fa05f41ed496206a55f63c195d32d61d425d1d7944
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted: unknown.

# pid = host and ipc=host
foo@linux:~> docker run -d --pid=host --ipc=host busybox sleep 1234
833e8b848cb44c6576636ac8f025407dca1f4451c69b0af09bb5d7d110cfa610
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted: unknown.

# ipc = host
foo@linux:~> docker run -d --ipc=host busybox sleep 1234
85612cffbcb9e31562062fbc50557960a60c68429c52b1b30fb6498385764669
docker: Error response from daemon: 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/6), flags: 0xe: operation not permitted: unknown. 
```
This blog post mentions pid and ipc namespace sharing with host won't work with rootless docker: https://raesene.github.io/blog/2020/12/19/rootless_docker/

The [Known Limitations for rootless docker](https://docs.docker.com/engine/security/rootless/#known-limitations) nor the [Known Limitations for user-ns remap](https://docs.docker.com/engine/security/userns-remap/#user-namespace-known-limitations) do not specify a limitation on the ipc=host configuration explicitly. 

```
Client:
 Version:           20.10.21
 API version:       1.41
 Go version:        go1.18.7
 Git commit:        baeda1f
 Built:             Tue Oct 25 17:56:30 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.21
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.18.7
  Git commit:       3056208
  Built:            Tue Oct 25 18:02:03 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.6.9
  GitCommit:        1c90a442489720eec95342e1789ee8a5e1b9536f
 runc:
  Version:          1.1.4
  GitCommit:        v1.1.4-0-g5fd4c4d1
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


Discussion / hints:
Yes, we should at least have this documented.  Would you like to contribute to the documentation?  Here is the markdown where the know limitations are described: https://github.com/docker/docs/blob/main/engine/security/rootless.md
I opened https://github.com/docker/docs/pull/16385. @sam-thibault Are there any plans to support sharing the IPC namespace with the host? There are use cases where it is desired in order to coordinate access to a shared memory file that is used between processes inside a container and outside the container. It is desirable for the processes in the container to be ran in rootless mode to implement the principle of least privilege.

Fix patch:
diff --git a/pkg/rootless/specconv/specconv_linux.go b/pkg/rootless/specconv/specconv_linux.go
index 16200e22e93fb..b706b5ca6a5f1 100644
--- a/pkg/rootless/specconv/specconv_linux.go
+++ b/pkg/rootless/specconv/specconv_linux.go
@@ -1,8 +1,10 @@
 package specconv // import "github.com/docker/docker/pkg/rootless/specconv"
 
 import (
+	"fmt"
 	"os"
 	"path"
+	"path/filepath"
 	"strconv"
 	"strings"
 
@@ -14,6 +16,7 @@ import (
 // * 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 {
@@ -79,31 +82,48 @@ func toRootless(spec *specs.Spec, v2Controllers []string, currentOOMScoreAdj int
 	}
 
 	// Fix up /proc if --pid=host
-	pidHost, err := isPidHost(spec)
+	pidHost, err := isHostNS(spec, specs.PIDNamespace)
 	if err != nil {
 		return err
 	}
-	if !pidHost {
-		return nil
+	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
 	}
-	return bindMountHostProcfs(spec)
+	if ipcHost {
+		if err := bindMountHostIPC(spec); err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
-func isPidHost(spec *specs.Spec) (bool, error) {
+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 == specs.PIDNamespace {
+		if ns.Type == nsType {
 			if ns.Path == "" {
 				return false, nil
 			}
-			pidNS, err := os.Readlink(ns.Path)
+			ns, err := os.Readlink(ns.Path)
 			if err != nil {
 				return false, err
 			}
-			selfPidNS, err := os.Readlink("/proc/self/ns/pid")
+			selfNS, err := os.Readlink(filepath.Join("/proc/self/ns", string(nsType)))
 			if err != nil {
 				return false, err
 			}
-			return pidNS == selfPidNS, nil
+			return ns == selfNS, nil
 		}
 	}
 	return true, nil
@@ -136,3 +156,22 @@ func bindMountHostProcfs(spec *specs.Spec) error {
 
 	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
+}


Test patch:
diff --git a/integration/container/ipcmode_linux_test.go b/integration/container/ipcmode_linux_test.go
index 49141ce58e82b..61d30b3360426 100644
--- a/integration/container/ipcmode_linux_test.go
+++ b/integration/container/ipcmode_linux_test.go
@@ -115,7 +115,7 @@ func TestIpcModePrivate(t *testing.T) {
 // also exists on the host.
 func TestIpcModeShareable(t *testing.T) {
 	skip.If(t, testEnv.IsRemoteDaemon)
-	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
+	skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless")
 
 	testIpcNonePrivateShareable(t, "shareable", true, true)
 }
@@ -191,7 +191,6 @@ func TestAPIIpcModeShareableAndContainer(t *testing.T) {
 func TestAPIIpcModeHost(t *testing.T) {
 	skip.If(t, testEnv.IsRemoteDaemon)
 	skip.If(t, testEnv.IsUserNamespace)
-	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
 
 	cfg := containertypes.Config{
 		Image: "busybox",
@@ -263,7 +262,7 @@ func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...strin
 // TestDaemonIpcModeShareable checks that --default-ipc-mode shareable works as intended.
 func TestDaemonIpcModeShareable(t *testing.T) {
 	skip.If(t, testEnv.IsRemoteDaemon)
-	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
+	skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless")
 
 	testDaemonIpcPrivateShareable(t, true, "--default-ipc-mode", "shareable")
 }
@@ -277,9 +276,9 @@ func TestDaemonIpcModePrivate(t *testing.T) {
 
 // used to check if an IpcMode given in config works as intended
 func testDaemonIpcFromConfig(t *testing.T, mode string, mustExist bool) {
-	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
 	config := `{"default-ipc-mode": "` + mode + `"}`
-	file := fs.NewFile(t, "test-daemon-ipc-config", fs.WithContent(config))
+	// WithMode is needed for rootless
+	file := fs.NewFile(t, "test-daemon-ipc-config", fs.WithContent(config), fs.WithMode(0o644))
 	defer file.Remove()
 
 	testDaemonIpcPrivateShareable(t, mustExist, "--config-file", file.Path())
@@ -295,6 +294,7 @@ func TestDaemonIpcModePrivateFromConfig(t *testing.T) {
 // TestDaemonIpcModeShareableFromConfig checks that "default-ipc-mode: shareable" config works as intended.
 func TestDaemonIpcModeShareableFromConfig(t *testing.T) {
 	skip.If(t, testEnv.IsRemoteDaemon)
+	skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless")
 
 	testDaemonIpcFromConfig(t, "shareable", true)
 }