Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
Expand All @@ -46,4 +46,4 @@ jobs:
make

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
2 changes: 1 addition & 1 deletion .github/workflows/scorecards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ jobs:

# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # tag=v4.32.1
uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # tag=v4.32.2
with:
sarif_file: results.sarif
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ require (
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
github.com/intel/goresctrl v0.11.0
github.com/klauspost/compress v1.18.3
github.com/klauspost/compress v1.18.4
github.com/mdlayher/vsock v1.2.1
github.com/moby/locker v1.0.1
github.com/moby/sys/mountinfo v0.7.2
Expand Down Expand Up @@ -75,9 +75,9 @@ require (
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
go.uber.org/goleak v1.3.0
golang.org/x/mod v0.32.0
golang.org/x/mod v0.33.0
golang.org/x/sync v0.19.0
golang.org/x/sys v0.40.0
golang.org/x/sys v0.41.0
golang.org/x/time v0.14.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217
google.golang.org/grpc v1.78.0
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/knqyf263/go-plugin v0.9.0 h1:CQs2+lOPIlkZVtcb835ZYDEoyyWJWLbSTWeCs0EwTwI=
github.com/knqyf263/go-plugin v0.9.0/go.mod h1:2z5lCO1/pez6qGo8CvCxSlBFSEat4MEp1DrnA+f7w8Q=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down Expand Up @@ -410,8 +410,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -470,8 +470,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand Down Expand Up @@ -508,8 +508,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
67 changes: 67 additions & 0 deletions integration/image_volume_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/containerd/containerd/v2/integration/images"
kernel "github.com/containerd/containerd/v2/pkg/kernelversion"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/sys"
"github.com/containerd/errdefs"
"github.com/opencontainers/image-spec/identity"
"github.com/opencontainers/selinux/go-selinux"
Expand Down Expand Up @@ -324,3 +325,69 @@ func TestImageVolumeSetupIfContainerdRestarts(t *testing.T) {
})
}
}

func TestImageVolumeWithUserNamespace(t *testing.T) {
// Check if user namespace and idmap are supported
if !supportsUserNS() {
t.Skip("user namespace not supported")
}

// Check if pidfd is supported
if !sys.SupportsPidFD() {
t.Skip("pidfd not supported")
}

if !supportsIDMap(defaultRoot) {
t.Skipf("idmap mounts not supported on: %s", defaultRoot)
}

containerID := uint32(0)
hostID := uint32(65536)
size := uint32(65536)

containerImage := images.Get(images.Alpine)
imageVolumeImage := images.Get(images.Pause)

podLogDir := t.TempDir()
podOpts := []PodSandboxOpts{
WithPodLogDirectory(podLogDir),
WithPodUserNs(containerID, hostID, size),
}
podCtx := newPodTCtx(t, runtimeService, t.Name(), "image-volume-userns", podOpts...)
defer podCtx.stop(true)

pullImagesByCRI(t, imageService, containerImage, imageVolumeImage)

// Create a container with image volume mount
// Pass the user namespace ID mappings to the image volume mount so that
// idmap is applied and files appear with correct ownership in the container
uidMaps := []*criruntime.IDMapping{{ContainerId: containerID, HostId: hostID, Length: size}}
gidMaps := []*criruntime.IDMapping{{ContainerId: containerID, HostId: hostID, Length: size}}

containerName := "test-container"
cfg := ContainerConfig(containerName, containerImage,
WithCommand("sleep", "1d"),
WithIDMapImageVolumeMount(imageVolumeImage, "", "/image-mount", uidMaps, gidMaps),
WithLogPath(containerName),
WithUserNamespace(containerID, hostID, size),
)
cnID, err := podCtx.rSvc.CreateContainer(podCtx.id, cfg, podCtx.cfg)
require.NoError(t, err, "failed to create container with image volume and user namespace")

require.NoError(t, podCtx.rSvc.StartContainer(cnID), "failed to start container")

// Verify that the image volume is accessible
stdout, stderr, err := runtimeService.ExecSync(cnID, []string{"ls", "/image-mount/pause"}, 0)
require.NoError(t, err, "failed to access image volume")
require.Len(t, stderr, 0)
require.Contains(t, string(stdout), "pause", "image volume should contain pause binary")

_, _, err = runtimeService.ExecSync(cnID, []string{"rm", "/image-mount/pause"}, 0)
require.Error(t, err, "image volume should be read-only")
require.Contains(t, err.Error(), "Read-only file system", "error should indicate read-only filesystem")

stdout, stderr, err = runtimeService.ExecSync(cnID, []string{"stat", "-c", "=%u=%g=", "/image-mount/pause"}, 0)
require.NoError(t, err, "failed to stat file in image volume")
require.Len(t, stderr, 0)
require.Contains(t, string(stdout), "=0=0=", "files in image volume should appear as owned by root in container's user namespace")
}
17 changes: 16 additions & 1 deletion internal/cri/server/container_image_mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,14 @@ func (c *criService) mutateImageMount(
}
chainID := identity.ChainID(diffIDs).String()

// Get snapshot options with user namespace idmap labels if needed
snapshotOpts, err := c.getImageVolumeSnapshotOpts(ctx, extraMount)
if err != nil {
return fmt.Errorf("failed to get snapshot options for image volume: %w", err)
}

s := c.client.SnapshotService(snapshotter)
mounts, err := s.Prepare(ctx, target, chainID)
mounts, err := s.Prepare(ctx, target, chainID, snapshotOpts...)
if err != nil {
if errdefs.IsAlreadyExists(err) {
mounts, err = s.Mounts(ctx, target)
Expand Down Expand Up @@ -160,6 +166,15 @@ func (c *criService) mutateImageMount(
}

extraMount.HostPath = target

// Clear UID/GID mappings from the mount to prevent the OCI runtime from
// attempting idmap on the bind mount. The idmap is already applied to the
// overlay lower layers via the snapshotter when the image volume is prepared.
// This must be done regardless of whether the image volume was already mounted
// (e.g., by another container in the same pod).
extraMount.UidMappings = nil
extraMount.GidMappings = nil

return nil
}

Expand Down
27 changes: 27 additions & 0 deletions internal/cri/server/container_image_mount_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
package server

import (
"context"
"fmt"
"os"
"sync"

containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
kernel "github.com/containerd/containerd/v2/pkg/kernelversion"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
)

var (
Expand Down Expand Up @@ -90,3 +94,26 @@ func ensureImageVolumeMounted(target string) (bool, error) {
}
return true, nil
}

// getImageVolumeSnapshotOpts returns snapshot options with user namespace idmap labels
// from the mount's UID/GID mappings. This ensures that image volumes work correctly
// with user namespaces by applying idmap to the overlay lower layers.
func (c *criService) getImageVolumeSnapshotOpts(ctx context.Context, mount *runtime.Mount) ([]snapshots.Opt, error) {
uids, err := parseUsernsIDMap(mount.GetUidMappings())
if err != nil {
return nil, fmt.Errorf("failed to parse UID mappings: %w", err)
}

gids, err := parseUsernsIDMap(mount.GetGidMappings())
if err != nil {
return nil, fmt.Errorf("failed to parse GID mappings: %w", err)
}

if len(uids) == 0 || len(gids) == 0 {
return nil, nil
}

return []snapshots.Opt{
containerd.WithRemapperLabels(0, uids[0].HostID, 0, gids[0].HostID, uids[0].Size),
}, nil
}
148 changes: 148 additions & 0 deletions internal/cri/server/container_image_mount_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//go:build linux

/*
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 server

import (
"context"
"testing"

containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
)

func TestGetImageVolumeSnapshotOpts(t *testing.T) {
ctx := context.Background()

mappings := []*runtime.IDMapping{
{
ContainerId: 0,
HostId: 65536,
Length: 65536,
},
}

expectedOpts := optsToInfo(t, containerd.WithRemapperLabels(0, 65536, 0, 65536, 65536))

for _, test := range []struct {
name string
mount *runtime.Mount
expectInfo *snapshots.Info
expectError bool
}{
{
name: "with user namespace mappings",
mount: &runtime.Mount{
ContainerPath: "/test",
UidMappings: mappings,
GidMappings: mappings,
},
expectInfo: expectedOpts,
},
{
name: "without mappings",
mount: &runtime.Mount{
ContainerPath: "/test",
},
},
{
name: "with empty mappings",
mount: &runtime.Mount{
ContainerPath: "/test",
UidMappings: []*runtime.IDMapping{},
GidMappings: []*runtime.IDMapping{},
},
},
{
name: "with only UID mappings",
mount: &runtime.Mount{
ContainerPath: "/test",
UidMappings: mappings,
},
},
{
name: "with only GID mappings",
mount: &runtime.Mount{
ContainerPath: "/test",
GidMappings: mappings,
},
},
{
name: "with multiple UID mapping lines",
mount: &runtime.Mount{
ContainerPath: "/test",
UidMappings: []*runtime.IDMapping{
{
ContainerId: 0,
HostId: 65536,
Length: 65536,
},
{
ContainerId: 65536,
HostId: 131072,
Length: 65536,
},
},
GidMappings: mappings,
},
expectError: true,
},
} {
t.Run(test.name, func(t *testing.T) {
c := &criService{}

opts, err := c.getImageVolumeSnapshotOpts(ctx, test.mount)

if test.expectError {
assert.Error(t, err)
return
}
require.NoError(t, err)

gotInfo := optsToInfo(t, opts...)
if test.expectInfo == nil {
assert.Nil(t, gotInfo)
} else {
require.NotNil(t, gotInfo)
assert.Equal(t, test.expectInfo.Labels, gotInfo.Labels)
}

if test.mount.UidMappings != nil {
assert.NotNil(t, test.mount.UidMappings, "UidMappings should NOT be cleared by getImageVolumeSnapshotOpts")
}
if test.mount.GidMappings != nil {
assert.NotNil(t, test.mount.GidMappings, "GidMappings should NOT be cleared by getImageVolumeSnapshotOpts")
}
})
}
}

func optsToInfo(t *testing.T, opts ...snapshots.Opt) *snapshots.Info {
t.Helper()
if len(opts) == 0 {
return nil
}
var info snapshots.Info
for _, opt := range opts {
require.NoError(t, opt(&info))
}
return &info
}
Loading
Loading