From 1e0f03b046c15f5f16e5eefe26cdaf88ee4342b9 Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <gscrivan@redhat.com>
Date: Fri, 20 Dec 2024 09:31:15 +0100
Subject: [PATCH 1/2] specgen: fix comment

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
---
 pkg/namespaces/namespaces.go | 2 +-
 pkg/specgen/namespaces.go    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go
index 3c0622c7d5..fa63433250 100644
--- a/pkg/namespaces/namespaces.go
+++ b/pkg/namespaces/namespaces.go
@@ -20,7 +20,7 @@ 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
diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go
index 689d68110c..aea39780b8 100644
--- a/pkg/specgen/namespaces.go
+++ b/pkg/specgen/namespaces.go
@@ -56,7 +56,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"

From b0b1d1972351054a09a044088f8ed942422ef8da Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <gscrivan@redhat.com>
Date: Fri, 20 Dec 2024 10:03:56 +0100
Subject: [PATCH 2/2] namespaces: allow configuring keep-id userns size

Introduce a new option "size" to configure the maximum size of the
user namespace configured by keep-id.

Closes: https://github.com/containers/podman/issues/24837

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
---
 .../markdown/options/userns.container.md      |  3 +-
 pkg/namespaces/namespaces.go                  |  9 +++
 pkg/specgen/namespaces.go                     |  4 ++
 pkg/util/utils.go                             | 13 +++-
 pkg/util/utils_test.go                        | 63 ++++++++++++++++++-
 test/e2e/run_userns_test.go                   | 12 ++++
 6 files changed, 100 insertions(+), 4 deletions(-)

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 fa63433250..4a9da30d62 100644
--- a/pkg/namespaces/namespaces.go
+++ b/pkg/namespaces/namespaces.go
@@ -26,6 +26,8 @@ type KeepIDUserNsOptions struct {
 	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 aea39780b8..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"
@@ -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()