diff --git a/internal/cri/opts/spec_opts.go b/internal/cri/opts/spec_opts.go index 0d78d54673..adb7ce7c04 100644 --- a/internal/cri/opts/spec_opts.go +++ b/internal/cri/opts/spec_opts.go @@ -344,17 +344,103 @@ func WithNamespacePath(t runtimespec.LinuxNamespaceType, nsPath string) oci.Spec } } -// WithPodNamespaces sets the pod namespaces for the container +// PodNamespacePaths are the paths of the pod namespaces a container joins. +// +// A pod sandbox is either PID-backed (a process such as pause is a member of +// every pod namespace, and the paths are derived from its pid with +// PodNamespacePathsFromPid) or path-backed (the sandbox holds the namespaces +// without a process and reports their paths, typically bind-mounted namespace +// files). An empty path means the sandbox does not hold that namespace. +type PodNamespacePaths struct { + Network string + IPC string + UTS string + // PID is the pod PID namespace, joined only when the pod shares it + // (NamespaceMode_POD or NamespaceMode_TARGET). The container keeps its own + // PID namespace otherwise. It is empty for a host PID pod on a sandbox + // without a process: the containers are then in the host PID namespace. + PID string + // PrivatePID is set by a sandbox that holds no PID namespace and declares + // that each container gets a new one, even when the pod asked to share + // (a sandbox without a process has no PID 1 to hold a shared namespace). + // It is an explicit declaration in the sandbox spec, not a fallback. + PrivatePID bool + User string +} + +// PodNamespacePathsFromPid derives the pod namespace paths from the pid of a +// process that is a member of every pod namespace, e.g. the pause container. +// targetPid is the process whose PID namespace the container joins when the +// pod shares one; it differs from sandboxPid for NamespaceMode_TARGET. +func PodNamespacePathsFromPid(sandboxPid, targetPid uint32) PodNamespacePaths { + return PodNamespacePaths{ + Network: GetNetworkNamespace(sandboxPid), + IPC: GetIPCNamespace(sandboxPid), + UTS: GetUTSNamespace(sandboxPid), + PID: GetPIDNamespace(targetPid), + User: GetUserNamespace(sandboxPid), + } +} + +// WithPodNamespaces sets the pod namespaces for the container from the pid of +// the sandbox process. See WithPodNamespacePaths. func WithPodNamespaces(config *runtime.LinuxContainerSecurityContext, sandboxPid uint32, targetPid uint32, uids, gids []runtimespec.LinuxIDMapping) oci.SpecOpts { + return WithPodNamespacePaths(config, config.GetNamespaceOptions(), PodNamespacePathsFromPid(sandboxPid, targetPid), uids, gids) +} + +// WithPodNamespacePaths makes the container join the pod namespaces at the +// given paths. The network, IPC and UTS namespaces of a container are always +// those of its pod, whatever the container config says (with a pause +// container the container joined the namespaces of pause, which was itself in +// the host namespaces the pod asked for); pod is the namespace options of the +// pod sandbox and decides which of them are host namespaces. The PID and user +// namespaces follow the container config as before; a container sharing the +// pod PID namespace (NamespaceMode_POD) is in the host one when the pod is. +// +// Every namespace the container is expected to share with the pod must have a +// path; a missing path is an error rather than a fallback to a fresh or host +// namespace, so that a broken sandbox cannot silently weaken isolation. The +// entry of a host namespace (NamespaceMode_NODE) is removed from the spec +// when the sandbox provides no path for it. +func WithPodNamespacePaths(config *runtime.LinuxContainerSecurityContext, pod *runtime.NamespaceOption, paths PodNamespacePaths, uids, gids []runtimespec.LinuxIDMapping) oci.SpecOpts { namespaces := config.GetNamespaceOptions() - opts := []oci.SpecOpts{ - oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.NetworkNamespace, Path: GetNetworkNamespace(sandboxPid)}), - oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.IPCNamespace, Path: GetIPCNamespace(sandboxPid)}), - oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.UTSNamespace, Path: GetUTSNamespace(sandboxPid)}), + var opts []oci.SpecOpts + join := func(t runtimespec.LinuxNamespaceType, path string, host bool) error { + switch { + case path != "": + opts = append(opts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: t, Path: path})) + case host: + opts = append(opts, WithoutNamespace(t)) + default: + return fmt.Errorf("pod sandbox provides no %s namespace for the container to join", t) + } + return nil } - if namespaces.GetPid() != runtime.NamespaceMode_CONTAINER { - opts = append(opts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.PIDNamespace, Path: GetPIDNamespace(targetPid)})) + + hostNetwork := pod.GetNetwork() == runtime.NamespaceMode_NODE + if err := join(runtimespec.NetworkNamespace, paths.Network, hostNetwork); err != nil { + return failSpecOpt(err) + } + if err := join(runtimespec.IPCNamespace, paths.IPC, pod.GetIpc() == runtime.NamespaceMode_NODE); err != nil { + return failSpecOpt(err) + } + // There is no CRI option for the UTS namespace: a pod shares it unless it + // uses the host network, in which case it uses the host UTS namespace too. + if err := join(runtimespec.UTSNamespace, paths.UTS, hostNetwork); err != nil { + return failSpecOpt(err) + } + // The default spec already gives the container its own PID namespace, + // which is what NamespaceMode_CONTAINER asks for and what a sandbox that + // declared it holds no PID namespace to share provides. A container that + // shares the pod PID namespace is in the host one when the pod is (pause + // was): a sandbox without a process provides no path for it. + pidMode := namespaces.GetPid() + if pidMode != runtime.NamespaceMode_CONTAINER && (pidMode != runtime.NamespaceMode_POD || !paths.PrivatePID) { + hostPID := pidMode == runtime.NamespaceMode_NODE || (pidMode == runtime.NamespaceMode_POD && pod.GetPid() == runtime.NamespaceMode_NODE) + if err := join(runtimespec.PIDNamespace, paths.PID, hostPID); err != nil { + return failSpecOpt(err) + } } if namespaces.GetUsernsOptions() != nil { @@ -362,7 +448,9 @@ func WithPodNamespaces(config *runtime.LinuxContainerSecurityContext, sandboxPid case runtime.NamespaceMode_NODE: // Nothing to do. Not adding userns field uses the node userns. case runtime.NamespaceMode_POD: - opts = append(opts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.UserNamespace, Path: GetUserNamespace(sandboxPid)})) + if err := join(runtimespec.UserNamespace, paths.User, false); err != nil { + return failSpecOpt(err) + } opts = append(opts, oci.WithUserNamespace(uids, gids)) } } @@ -370,6 +458,12 @@ func WithPodNamespaces(config *runtime.LinuxContainerSecurityContext, sandboxPid return oci.Compose(opts...) } +func failSpecOpt(err error) oci.SpecOpts { + return func(context.Context, oci.Client, *containers.Container, *runtimespec.Spec) error { + return err + } +} + const ( // netNSFormat is the format of network namespace of a process. netNSFormat = "/proc/%v/ns/net" diff --git a/internal/cri/opts/spec_opts_namespaces_test.go b/internal/cri/opts/spec_opts_namespaces_test.go new file mode 100644 index 0000000000..6d9c568269 --- /dev/null +++ b/internal/cri/opts/spec_opts_namespaces_test.go @@ -0,0 +1,247 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package opts + +import ( + "context" + "testing" + + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1" + + "github.com/containerd/containerd/v2/pkg/oci" +) + +// defaultNamespaces are the namespaces of the default OCI spec: private, no +// path, before the pod namespaces are applied. +func defaultNamespaces() *runtimespec.Spec { + return &runtimespec.Spec{Linux: &runtimespec.Linux{Namespaces: []runtimespec.LinuxNamespace{ + {Type: runtimespec.PIDNamespace}, + {Type: runtimespec.IPCNamespace}, + {Type: runtimespec.UTSNamespace}, + {Type: runtimespec.MountNamespace}, + {Type: runtimespec.NetworkNamespace}, + }}} +} + +func applyNamespaces(t *testing.T, opt oci.SpecOpts) (*runtimespec.Spec, error) { + t.Helper() + spec := defaultNamespaces() + err := opt(context.Background(), nil, nil, spec) + return spec, err +} + +func namespacePaths(spec *runtimespec.Spec) map[runtimespec.LinuxNamespaceType]string { + paths := make(map[runtimespec.LinuxNamespaceType]string) + for _, ns := range spec.Linux.Namespaces { + paths[ns.Type] = ns.Path + } + return paths +} + +func securityContext(network, ipc, pid runtime.NamespaceMode) *runtime.LinuxContainerSecurityContext { + return &runtime.LinuxContainerSecurityContext{NamespaceOptions: &runtime.NamespaceOption{ + Network: network, + Ipc: ipc, + Pid: pid, + }} +} + +func TestWithPodNamespacePathsFromPid(t *testing.T) { + // A sandbox with a process (pause) keeps today's construction exactly, + // whatever the requested modes: the container joins the namespaces of the + // sandbox process, which is itself in the host namespaces the pod asked for. + for name, sc := range map[string]*runtime.LinuxContainerSecurityContext{ + "pod namespaces": securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_POD), + "host namespaces": securityContext(runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE), + "container pid": securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER), + } { + t.Run(name, func(t *testing.T) { + legacy, err := applyNamespaces(t, WithPodNamespaces(sc, 42, 42, nil, nil)) + require.NoError(t, err) + byPath, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePathsFromPid(42, 42), nil, nil)) + require.NoError(t, err) + assert.Equal(t, legacy, byPath) + + paths := namespacePaths(byPath) + assert.Equal(t, "/proc/42/ns/net", paths[runtimespec.NetworkNamespace]) + assert.Equal(t, "/proc/42/ns/ipc", paths[runtimespec.IPCNamespace]) + assert.Equal(t, "/proc/42/ns/uts", paths[runtimespec.UTSNamespace]) + if sc.GetNamespaceOptions().GetPid() == runtime.NamespaceMode_CONTAINER { + assert.Equal(t, "", paths[runtimespec.PIDNamespace], "the container keeps its own PID namespace") + } else { + assert.Equal(t, "/proc/42/ns/pid", paths[runtimespec.PIDNamespace]) + } + }) + } + + t.Run("target pid", func(t *testing.T) { + sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_TARGET) + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePathsFromPid(42, 7), nil, nil)) + require.NoError(t, err) + assert.Equal(t, "/proc/7/ns/pid", namespacePaths(spec)[runtimespec.PIDNamespace]) + }) +} + +func TestWithPodNamespacePaths(t *testing.T) { + pathBacked := PodNamespacePaths{ + Network: "/var/run/netns/cni-1", + IPC: "/run/sandbox/ns/ipc", + UTS: "/run/sandbox/ns/uts", + } + + t.Run("pod namespaces by path", func(t *testing.T) { + sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER) + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), pathBacked, nil, nil)) + require.NoError(t, err) + assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{ + runtimespec.NetworkNamespace: pathBacked.Network, + runtimespec.IPCNamespace: pathBacked.IPC, + runtimespec.UTSNamespace: pathBacked.UTS, + runtimespec.PIDNamespace: "", + runtimespec.MountNamespace: "", + }, namespacePaths(spec)) + }) + + t.Run("host namespaces without paths are removed from the spec", func(t *testing.T) { + sc := securityContext(runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE) + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePaths{}, nil, nil)) + require.NoError(t, err) + assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{ + runtimespec.MountNamespace: "", + }, namespacePaths(spec), "only the private mount namespace is left") + }) + + t.Run("a sandbox declaring private PID namespaces", func(t *testing.T) { + // A pod asking to share its PID namespace on a sandbox that holds + // none: the sandbox said so explicitly, the container keeps its own. + sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_POD) + paths := pathBacked + paths.PrivatePID = true + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, nil, nil)) + require.NoError(t, err) + assert.Equal(t, "", namespacePaths(spec)[runtimespec.PIDNamespace]) + assert.Contains(t, namespacePaths(spec), runtimespec.PIDNamespace) + + // The declaration does not cover the host PID namespace. + sc = securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_NODE) + spec, err = applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, nil, nil)) + require.NoError(t, err) + assert.NotContains(t, namespacePaths(spec), runtimespec.PIDNamespace) + + // Nor a container joining the PID namespace of another container. + sc = securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_TARGET) + paths.PID = "/proc/7/ns/pid" + spec, err = applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, nil, nil)) + require.NoError(t, err) + assert.Equal(t, paths.PID, namespacePaths(spec)[runtimespec.PIDNamespace]) + }) + + t.Run("host pid pod", func(t *testing.T) { + // A sandbox without a process holds no PID namespace for a host PID + // pod and provides no path: a container sharing the pod PID namespace + // (NamespaceMode_POD, the zero value) is in the host one like the + // pod, as it was with pause; NamespaceMode_CONTAINER keeps its own. + pod := &runtime.NamespaceOption{Network: runtime.NamespaceMode_POD, Ipc: runtime.NamespaceMode_POD, Pid: runtime.NamespaceMode_NODE} + for _, mode := range []runtime.NamespaceMode{runtime.NamespaceMode_POD, runtime.NamespaceMode_NODE} { + sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, mode) + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, pod, pathBacked, nil, nil)) + require.NoError(t, err, "pid mode %s", mode) + assert.NotContains(t, namespacePaths(spec), runtimespec.PIDNamespace, "pid mode %s", mode) + } + sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER) + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, pod, pathBacked, nil, nil)) + require.NoError(t, err) + assert.Equal(t, "", namespacePaths(spec)[runtimespec.PIDNamespace]) + assert.Contains(t, namespacePaths(spec), runtimespec.PIDNamespace) + }) + + t.Run("host network with pod ipc", func(t *testing.T) { + sc := securityContext(runtime.NamespaceMode_NODE, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER) + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePaths{IPC: pathBacked.IPC}, nil, nil)) + require.NoError(t, err) + assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{ + runtimespec.IPCNamespace: pathBacked.IPC, + runtimespec.PIDNamespace: "", + runtimespec.MountNamespace: "", + }, namespacePaths(spec), "no network and no UTS namespace entry") + }) + + t.Run("the pod decides the host namespaces, not the container config", func(t *testing.T) { + // A container config that says nothing (NamespaceMode_POD, the zero + // value) in a host network pod: the container is in the host network + // and UTS namespaces like the pod, as it was with pause. + container := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER) + pod := &runtime.NamespaceOption{Network: runtime.NamespaceMode_NODE, Ipc: runtime.NamespaceMode_POD} + spec, err := applyNamespaces(t, WithPodNamespacePaths(container, pod, PodNamespacePaths{IPC: pathBacked.IPC}, nil, nil)) + require.NoError(t, err) + assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{ + runtimespec.IPCNamespace: pathBacked.IPC, + runtimespec.PIDNamespace: "", + runtimespec.MountNamespace: "", + }, namespacePaths(spec)) + }) + + // A pod namespace the sandbox does not provide is an error, never a + // silent fallback to a fresh or host namespace. + for name, tc := range map[string]struct { + sc *runtime.LinuxContainerSecurityContext + paths PodNamespacePaths + }{ + "missing network": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER), PodNamespacePaths{IPC: pathBacked.IPC, UTS: pathBacked.UTS}}, + "missing ipc": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER), PodNamespacePaths{Network: pathBacked.Network, UTS: pathBacked.UTS}}, + "missing uts": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER), PodNamespacePaths{Network: pathBacked.Network, IPC: pathBacked.IPC}}, + "missing pid": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_POD), pathBacked}, + "missing target pid": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_TARGET), PodNamespacePaths{ + Network: pathBacked.Network, + IPC: pathBacked.IPC, + UTS: pathBacked.UTS, + PrivatePID: true, + }}, + "missing user": {&runtime.LinuxContainerSecurityContext{NamespaceOptions: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_POD, + Ipc: runtime.NamespaceMode_POD, + Pid: runtime.NamespaceMode_CONTAINER, + UsernsOptions: &runtime.UserNamespace{Mode: runtime.NamespaceMode_POD}, + }}, pathBacked}, + } { + t.Run(name, func(t *testing.T) { + _, err := applyNamespaces(t, WithPodNamespacePaths(tc.sc, tc.sc.GetNamespaceOptions(), tc.paths, nil, nil)) + require.Error(t, err) + assert.Contains(t, err.Error(), "provides no") + }) + } + + t.Run("pod user namespace by path", func(t *testing.T) { + sc := &runtime.LinuxContainerSecurityContext{NamespaceOptions: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_POD, + Ipc: runtime.NamespaceMode_POD, + Pid: runtime.NamespaceMode_CONTAINER, + UsernsOptions: &runtime.UserNamespace{Mode: runtime.NamespaceMode_POD}, + }} + paths := pathBacked + paths.User = "/run/sandbox/ns/user" + uids := []runtimespec.LinuxIDMapping{{ContainerID: 0, HostID: 65536, Size: 65536}} + spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, uids, uids)) + require.NoError(t, err) + assert.Equal(t, paths.User, namespacePaths(spec)[runtimespec.UserNamespace]) + assert.Equal(t, uids, spec.Linux.UIDMappings) + assert.Equal(t, uids, spec.Linux.GIDMappings) + }) +}