diff --git a/docs/source/markdown/options/userns.container.md b/docs/source/markdown/options/userns.container.md index 515e9302fd..4ffc22c963 100644 --- a/docs/source/markdown/options/userns.container.md +++ b/docs/source/markdown/options/userns.container.md @@ -40,7 +40,7 @@ Podman allocates unique ranges of UIDs and GIDs from the `containers` subordinat The option `--userns=keep-id` uses all the subuids and subgids of the user. The option `--userns=nomap` uses all the subuids and subgids of the user except the user's own ID. -Using `--userns=auto` when starting new containers does not work as long as any containers exist that were started with `--userns=keep-id` or `--userns=nomap`. +Using `--userns=auto` when starting new containers does not work as long as any containers exist that were started with `--userns=nomap` or `--userns=keep-id` without limiting the user namespace size. Valid `auto` options: @@ -62,6 +62,7 @@ For details see **--uidmap**. - *uid*=UID: override the UID inside the container that is used to map the current user to. - *gid*=GID: override the GID inside the container that is used to map the current user to. + - *size*=SIZE: override the size of the configured user namespace. It is useful to not saturate all the available IDs. Not supported when running as root. **nomap**: creates a user namespace where the current rootless user's UID:GID are not mapped into the container. This option is not allowed for containers created by the root user. diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go index 3c0622c7d5..4a9da30d62 100644 --- a/pkg/namespaces/namespaces.go +++ b/pkg/namespaces/namespaces.go @@ -20,12 +20,14 @@ const ( pastaType = "pasta" ) -// KeepIDUserNsOptions defines how to keepIDmatically create a user namespace. +// KeepIDUserNsOptions defines how to create a user namespace using keep-id. type KeepIDUserNsOptions struct { // UID is the target uid in the user namespace. UID *uint32 // GID is the target uid in the user namespace. GID *uint32 + // MaxSize is the maximum size of the user namespace. + MaxSize *uint32 } // CgroupMode represents cgroup mode in the container. @@ -148,6 +150,13 @@ func (n UsernsMode) GetKeepIDOptions() (*KeepIDUserNsOptions, error) { } v := uint32(s) options.GID = &v + case "size": + s, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, err + } + v := uint32(s) + options.MaxSize = &v default: return nil, fmt.Errorf("unknown option specified: %q", opt) } diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index 689d68110c..1bf9f886e3 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -12,6 +12,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/pkg/namespaces" + "github.com/containers/podman/v5/pkg/rootless" "github.com/containers/podman/v5/pkg/util" "github.com/containers/storage/pkg/fileutils" "github.com/containers/storage/pkg/unshare" @@ -56,7 +57,7 @@ const ( // Pasta indicates that a pasta network stack should be used. // Only used with the network namespace, invalid otherwise. Pasta NamespaceMode = "pasta" - // KeepId indicates a user namespace to keep the owner uid inside + // KeepID indicates a user namespace to keep the owner uid inside // of the namespace itself. // Only used with the user namespace, invalid otherwise. KeepID NamespaceMode = "keep-id" @@ -514,6 +515,9 @@ func SetupUserNS(idmappings *storageTypes.IDMappingOptions, userns Namespace, g if err != nil { return user, err } + if opts.MaxSize != nil && !rootless.IsRootless() { + return user, fmt.Errorf("cannot set max size for user namespace when not running rootless") + } mappings, uid, gid, err := util.GetKeepIDMapping(opts) if err != nil { return user, err diff --git a/pkg/util/utils.go b/pkg/util/utils.go index ddfda73d13..00c2d19073 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -173,7 +173,7 @@ func ParseSignal(rawSignal string) (syscall.Signal, error) { return sig, nil } -func getRootlessKeepIDMapping(uid, gid int, uids, gids []idtools.IDMap) (*stypes.IDMappingOptions, int, int, error) { +func getRootlessKeepIDMapping(uid, gid int, uids, gids []idtools.IDMap, maxSize int) (*stypes.IDMappingOptions, int, int, error) { options := stypes.IDMappingOptions{ HostUIDMapping: false, HostGIDMapping: false, @@ -185,6 +185,11 @@ func getRootlessKeepIDMapping(uid, gid int, uids, gids []idtools.IDMap) (*stypes for _, g := range gids { maxGID += g.Size } + if maxSize > 0 { + // If maxSize is set, we need to ensure that the mappings are within the available range + maxUID = min(maxUID, maxSize-1) + maxGID = min(maxGID, maxSize-1) + } options.UIDMap, options.GIDMap = nil, nil @@ -240,13 +245,17 @@ func GetKeepIDMapping(opts *namespaces.KeepIDUserNsOptions) (*stypes.IDMappingOp if opts.GID != nil { gid = int(*opts.GID) } + maxSize := 0 + if opts.MaxSize != nil { + maxSize = int(*opts.MaxSize) + } uids, gids, err := rootless.GetConfiguredMappings(true) if err != nil { return nil, -1, -1, fmt.Errorf("cannot read mappings: %w", err) } - return getRootlessKeepIDMapping(uid, gid, uids, gids) + return getRootlessKeepIDMapping(uid, gid, uids, gids, maxSize) } // GetNoMapMapping returns the mappings and the user to use when nomap is used diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go index dd53ac1c36..a83adcdf6d 100644 --- a/pkg/util/utils_test.go +++ b/pkg/util/utils_test.go @@ -595,6 +595,7 @@ func TestGetRootlessKeepIDMapping(t *testing.T) { tests := []struct { uid, gid int uids, gids []idtools.IDMap + size int expectedOptions *stypes.IDMappingOptions expectedUID, expectedGID int expectedError error @@ -627,10 +628,70 @@ func TestGetRootlessKeepIDMapping(t *testing.T) { expectedUID: 0, expectedGID: 0, }, + { + uid: 0, + gid: 0, + uids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}}, + gids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}}, + expectedOptions: &stypes.IDMappingOptions{ + HostUIDMapping: false, + HostGIDMapping: false, + UIDMap: []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}, {ContainerID: 1, HostID: 1, Size: 1023}}, + GIDMap: []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}, {ContainerID: 1, HostID: 1, Size: 1023}}, + }, + expectedUID: 0, + expectedGID: 0, + size: 1024, + }, + { + uid: 0, + gid: 0, + uids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}}, + gids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}}, + expectedOptions: &stypes.IDMappingOptions{ + HostUIDMapping: false, + HostGIDMapping: false, + UIDMap: []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}}, + GIDMap: []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}}, + }, + expectedUID: 0, + expectedGID: 0, + size: 1, + }, + { + uid: 0, + gid: 0, + uids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}}, + gids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}}, + expectedOptions: &stypes.IDMappingOptions{ + HostUIDMapping: false, + HostGIDMapping: false, + UIDMap: []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}, {ContainerID: 1, HostID: 1, Size: 1}}, + GIDMap: []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}, {ContainerID: 1, HostID: 1, Size: 1}}, + }, + expectedUID: 0, + expectedGID: 0, + size: 2, + }, + { + uid: 1000, + gid: 1000, + uids: []idtools.IDMap{}, + gids: []idtools.IDMap{}, + expectedOptions: &stypes.IDMappingOptions{ + HostUIDMapping: false, + HostGIDMapping: false, + UIDMap: []idtools.IDMap{{ContainerID: 1000, HostID: 0, Size: 1}}, + GIDMap: []idtools.IDMap{{ContainerID: 1000, HostID: 0, Size: 1}}, + }, + expectedUID: 1000, + expectedGID: 1000, + size: 1000000, + }, } for _, test := range tests { - options, uid, gid, err := getRootlessKeepIDMapping(test.uid, test.gid, test.uids, test.gids) + options, uid, gid, err := getRootlessKeepIDMapping(test.uid, test.gid, test.uids, test.gids, test.size) assert.Nil(t, err) assert.Equal(t, test.expectedOptions, options) assert.Equal(t, test.expectedUID, uid) diff --git a/test/e2e/run_userns_test.go b/test/e2e/run_userns_test.go index edac398588..00fd5d53f0 100644 --- a/test/e2e/run_userns_test.go +++ b/test/e2e/run_userns_test.go @@ -158,6 +158,18 @@ var _ = Describe("Podman UserNS support", func() { Expect(session.OutputToString()).To(Equal("0")) }) + It("podman --userns=keep-id:size", func() { + session := podmanTest.Podman([]string{"run", "--userns=keep-id:size=10", ALPINE, "sh", "-c", "(awk 'BEGIN{SUM=0} {SUM += $3} END{print SUM}' < /proc/self/uid_map)"}) + session.WaitWithDefaultTimeout() + + if isRootless() { + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(Equal("10")) + } else { + Expect(session).Should(ExitWithError(125, "cannot set max size for user namespace when not running rootless")) + } + }) + It("podman --userns=keep-id --user root:root", func() { session := podmanTest.Podman([]string{"run", "--userns=keep-id", "--user", "root:root", "alpine", "id", "-u"}) session.WaitWithDefaultTimeout()