@@ -28,6 +28,7 @@ import (
2828 "github.com/containerd/containerd/v2/integration/images"
2929 kernel "github.com/containerd/containerd/v2/pkg/kernelversion"
3030 "github.com/containerd/containerd/v2/pkg/namespaces"
31+ "github.com/containerd/containerd/v2/pkg/sys"
3132 "github.com/containerd/errdefs"
3233 "github.com/opencontainers/image-spec/identity"
3334 "github.com/opencontainers/selinux/go-selinux"
@@ -324,3 +325,69 @@ func TestImageVolumeSetupIfContainerdRestarts(t *testing.T) {
324325 })
325326 }
326327}
328+
329+ func TestImageVolumeWithUserNamespace (t * testing.T ) {
330+ // Check if user namespace and idmap are supported
331+ if ! supportsUserNS () {
332+ t .Skip ("user namespace not supported" )
333+ }
334+
335+ // Check if pidfd is supported
336+ if ! sys .SupportsPidFD () {
337+ t .Skip ("pidfd not supported" )
338+ }
339+
340+ if ! supportsIDMap (defaultRoot ) {
341+ t .Skipf ("idmap mounts not supported on: %s" , defaultRoot )
342+ }
343+
344+ containerID := uint32 (0 )
345+ hostID := uint32 (65536 )
346+ size := uint32 (65536 )
347+
348+ containerImage := images .Get (images .Alpine )
349+ imageVolumeImage := images .Get (images .Pause )
350+
351+ podLogDir := t .TempDir ()
352+ podOpts := []PodSandboxOpts {
353+ WithPodLogDirectory (podLogDir ),
354+ WithPodUserNs (containerID , hostID , size ),
355+ }
356+ podCtx := newPodTCtx (t , runtimeService , t .Name (), "image-volume-userns" , podOpts ... )
357+ defer podCtx .stop (true )
358+
359+ pullImagesByCRI (t , imageService , containerImage , imageVolumeImage )
360+
361+ // Create a container with image volume mount
362+ // Pass the user namespace ID mappings to the image volume mount so that
363+ // idmap is applied and files appear with correct ownership in the container
364+ uidMaps := []* criruntime.IDMapping {{ContainerId : containerID , HostId : hostID , Length : size }}
365+ gidMaps := []* criruntime.IDMapping {{ContainerId : containerID , HostId : hostID , Length : size }}
366+
367+ containerName := "test-container"
368+ cfg := ContainerConfig (containerName , containerImage ,
369+ WithCommand ("sleep" , "1d" ),
370+ WithIDMapImageVolumeMount (imageVolumeImage , "" , "/image-mount" , uidMaps , gidMaps ),
371+ WithLogPath (containerName ),
372+ WithUserNamespace (containerID , hostID , size ),
373+ )
374+ cnID , err := podCtx .rSvc .CreateContainer (podCtx .id , cfg , podCtx .cfg )
375+ require .NoError (t , err , "failed to create container with image volume and user namespace" )
376+
377+ require .NoError (t , podCtx .rSvc .StartContainer (cnID ), "failed to start container" )
378+
379+ // Verify that the image volume is accessible
380+ stdout , stderr , err := runtimeService .ExecSync (cnID , []string {"ls" , "/image-mount/pause" }, 0 )
381+ require .NoError (t , err , "failed to access image volume" )
382+ require .Len (t , stderr , 0 )
383+ require .Contains (t , string (stdout ), "pause" , "image volume should contain pause binary" )
384+
385+ _ , _ , err = runtimeService .ExecSync (cnID , []string {"rm" , "/image-mount/pause" }, 0 )
386+ require .Error (t , err , "image volume should be read-only" )
387+ require .Contains (t , err .Error (), "Read-only file system" , "error should indicate read-only filesystem" )
388+
389+ stdout , stderr , err = runtimeService .ExecSync (cnID , []string {"stat" , "-c" , "=%u=%g=" , "/image-mount/pause" }, 0 )
390+ require .NoError (t , err , "failed to stat file in image volume" )
391+ require .Len (t , stderr , 0 )
392+ require .Contains (t , string (stdout ), "=0=0=" , "files in image volume should appear as owned by root in container's user namespace" )
393+ }
0 commit comments