From ecff1401cb1032053425e9be777ab597bd083e8f Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Tue, 8 Sep 2026 19:32:32 -0700 Subject: [PATCH] cri/opts: add WithPodNamespacePaths WithPodNamespaces derives the namespaces a container joins from the pid of the sandbox process. A sandbox without a process has to name the namespaces it holds instead. WithPodNamespacePaths takes the paths explicitly. The network, IPC and UTS namespaces of a container are always those of its pod, whatever the container config says, exactly as they were with pause; the pod's namespace options decide which of them are host namespaces. Every namespace the pod shares must have a path: a missing one is an error, never a fallback to a fresh or host namespace, unless the pod asked for the host namespace, in which case the entry is removed from the spec. A sandbox may declare that it holds no PID namespace and that each container gets its own. WithPodNamespaces delegates to it with the pid derived paths, so pause based pods produce the same specs as before. Signed-off-by: Maksym Pavlenko --- internal/cri/opts/spec_opts.go | 110 +++++++- .../cri/opts/spec_opts_namespaces_test.go | 247 ++++++++++++++++++ 2 files changed, 349 insertions(+), 8 deletions(-) create mode 100644 internal/cri/opts/spec_opts_namespaces_test.go 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) + }) +}