From d5cf46e80748bf85a853fc3a72229269f14e7d64 Mon Sep 17 00:00:00 2001 From: kaivol Date: Mon, 20 Nov 2023 17:21:11 +0100 Subject: [PATCH 1/2] support lookup of intermediate IDs in gidmapping/uidmapping options in userns=auto Closes #20699 Signed-off-by: kaivol --- .../markdown/options/userns.container.md | 4 + pkg/domain/infra/runtime_libpod.go | 2 +- pkg/namespaces/namespaces.go | 44 ----- pkg/util/utils.go | 158 +++++++++++++++++- pkg/util/utils_test.go | 36 ++++ 5 files changed, 198 insertions(+), 46 deletions(-) diff --git a/docs/source/markdown/options/userns.container.md b/docs/source/markdown/options/userns.container.md index ff975ccf2b..515e9302fd 100644 --- a/docs/source/markdown/options/userns.container.md +++ b/docs/source/markdown/options/userns.container.md @@ -48,6 +48,10 @@ Using `--userns=auto` when starting new containers does not work as long as any - *size*=_SIZE_: to specify an explicit size for the automatic user namespace. e.g. `--userns=auto:size=8192`. If `size` is not specified, `auto` estimates a size for the user namespace. - *uidmapping*=_CONTAINER\_UID:HOST\_UID:SIZE_: to force a UID mapping to be present in the user namespace. +The host UID and GID in *gidmapping* and *uidmapping* can optionally be prefixed with the `@` symbol. +In this case, podman will look up the intermediate ID corresponding to host ID and it will map the found intermediate ID to the container id. +For details see **--uidmap**. + **container:**_id_: join the user namespace of the specified container. **host** or **""** (empty string): run in the user namespace of the caller. The processes running in the container have the same privileges on the host as any other process launched by the calling user. diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index d598b1dd39..098efea6f6 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -282,7 +282,7 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin options.HostUIDMapping = false options.HostGIDMapping = false options.AutoUserNs = true - opts, err := mode.GetAutoOptions() + opts, err := util.GetAutoOptions(mode) if err != nil { return nil, err } diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go index 1ca1f9a2e0..2731fe95ad 100644 --- a/pkg/namespaces/namespaces.go +++ b/pkg/namespaces/namespaces.go @@ -4,8 +4,6 @@ import ( "fmt" "strconv" "strings" - - "github.com/containers/storage/types" ) const ( @@ -122,48 +120,6 @@ func (n UsernsMode) IsDefaultValue() bool { return n == "" || n == defaultType } -// GetAutoOptions returns an AutoUserNsOptions with the settings to automatically set up -// a user namespace. -func (n UsernsMode) GetAutoOptions() (*types.AutoUserNsOptions, error) { - parts := strings.SplitN(string(n), ":", 2) - if parts[0] != "auto" { - return nil, fmt.Errorf("wrong user namespace mode") - } - options := types.AutoUserNsOptions{} - if len(parts) == 1 { - return &options, nil - } - for _, o := range strings.Split(parts[1], ",") { - v := strings.SplitN(o, "=", 2) - if len(v) != 2 { - return nil, fmt.Errorf("invalid option specified: %q", o) - } - switch v[0] { - case "size": - s, err := strconv.ParseUint(v[1], 10, 32) - if err != nil { - return nil, err - } - options.Size = uint32(s) - case "uidmapping": - mapping, err := types.ParseIDMapping([]string{v[1]}, nil, "", "") - if err != nil { - return nil, err - } - options.AdditionalUIDMappings = append(options.AdditionalUIDMappings, mapping.UIDMap...) - case "gidmapping": - mapping, err := types.ParseIDMapping(nil, []string{v[1]}, "", "") - if err != nil { - return nil, err - } - options.AdditionalGIDMappings = append(options.AdditionalGIDMappings, mapping.GIDMap...) - default: - return nil, fmt.Errorf("unknown option specified: %q", v[0]) - } - } - return &options, nil -} - // GetKeepIDOptions returns a KeepIDUserNsOptions with the settings to keepIDmatically set up // a user namespace. func (n UsernsMode) GetKeepIDOptions() (*KeepIDUserNsOptions, error) { diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 0d4fa4ef4c..04abffa36f 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -830,6 +830,162 @@ func sortAndMergeConsecutiveMappings(idmap []idtools.IDMap) (finalIDMap []idtool return finalIDMap } +// Extension of idTools.parseAutoTriple that parses idmap triples. +// The triple should be a length 3 string array, containing: +// - Flags and ContainerID +// - HostID +// - Size +// +// parseAutoTriple returns the parsed mapping and any possible error. +// If the error is not-nil, the mapping is not well-defined. +// +// idTools.parseAutoTriple is extended here with the following enhancements: +// +// HostID @ syntax: +// ================= +// HostID may use the "@" syntax: The "101001:@1001:1" mapping +// means "take the 1001 id from the parent namespace and map it to 101001" +func parseAutoTriple(spec []string, parentMapping []ruser.IDMap, mapSetting string) (mappings []idtools.IDMap, err error) { + if len(spec[0]) == 0 { + return mappings, fmt.Errorf("invalid empty container id at %s map: %v", mapSetting, spec) + } + var cids, hids, sizes []uint64 + var cid, hid uint64 + var hidIsParent bool + // Parse the container ID, which must be an integer: + cid, err = strconv.ParseUint(spec[0][0:], 10, 32) + if err != nil { + return mappings, fmt.Errorf("parsing id map value %q: %w", spec[0], err) + } + // Parse the host id, which may be integer or @ + if len(spec[1]) == 0 { + return mappings, fmt.Errorf("invalid empty host id at %s map: %v", mapSetting, spec) + } + if spec[1][0] != '@' { + hidIsParent = false + hid, err = strconv.ParseUint(spec[1], 10, 32) + } else { + // Parse @, where is an integer corresponding to the parent mapping + hidIsParent = true + hid, err = strconv.ParseUint(spec[1][1:], 10, 32) + } + if err != nil { + return mappings, fmt.Errorf("parsing id map value %q: %w", spec[1], err) + } + // Parse the size of the mapping, which must be an integer + sz, err := strconv.ParseUint(spec[2], 10, 32) + if err != nil { + return mappings, fmt.Errorf("parsing id map value %q: %w", spec[2], err) + } + + if hidIsParent { + for i := uint64(0); i < sz; i++ { + cids = append(cids, cid+i) + mappedID, err := mapIDwithMapping(hid+i, parentMapping, mapSetting) + if err != nil { + return mappings, err + } + hids = append(hids, mappedID) + sizes = append(sizes, 1) + } + } else { + cids = []uint64{cid} + hids = []uint64{hid} + sizes = []uint64{sz} + } + + // Avoid possible integer overflow on 32bit builds + if bits.UintSize == 32 { + for i := range cids { + if cids[i] > math.MaxInt32 || hids[i] > math.MaxInt32 || sizes[i] > math.MaxInt32 { + return mappings, fmt.Errorf("initializing ID mappings: %s setting is malformed expected [\"[+ug]uint32:[@]uint32[:uint32]\"] : %q", mapSetting, spec) + } + } + } + for i := range cids { + mappings = append(mappings, idtools.IDMap{ + ContainerID: int(cids[i]), + HostID: int(hids[i]), + Size: int(sizes[i]), + }) + } + return mappings, nil +} + +// Extension of idTools.ParseIDMap that parses idmap triples from string. +// This extension accepts additional flags that control how the mapping is done +func parseAutoIDMap(mapSpec string, mapSetting string, parentMapping []ruser.IDMap) (idmap []idtools.IDMap, err error) { + stdErr := fmt.Errorf("initializing ID mappings: %s setting is malformed expected [\"uint32:[@]uint32[:uint32]\"] : %q", mapSetting, mapSpec) + idSpec := strings.Split(mapSpec, ":") + // if it's a length-2 list assume the size is 1: + if len(idSpec) == 2 { + idSpec = append(idSpec, "1") + } + if len(idSpec) != 3 { + return nil, stdErr + } + // Parse this mapping: + mappings, err := parseAutoTriple(idSpec, parentMapping, mapSetting) + if err != nil { + return nil, err + } + idmap = sortAndMergeConsecutiveMappings(mappings) + return idmap, nil +} + +// GetAutoOptions returns an AutoUserNsOptions with the settings to automatically set up +// a user namespace. +func GetAutoOptions(n namespaces.UsernsMode) (*stypes.AutoUserNsOptions, error) { + parts := strings.SplitN(string(n), ":", 2) + if parts[0] != "auto" { + return nil, fmt.Errorf("wrong user namespace mode") + } + options := stypes.AutoUserNsOptions{} + if len(parts) == 1 { + return &options, nil + } + + parentUIDMap, parentGIDMap, err := rootless.GetAvailableIDMaps() + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + // The kernel-provided files only exist if user namespaces are supported + logrus.Debugf("User or group ID mappings not available: %s", err) + } else { + return nil, err + } + } + + for _, o := range strings.Split(parts[1], ",") { + v := strings.SplitN(o, "=", 2) + if len(v) != 2 { + return nil, fmt.Errorf("invalid option specified: %q", o) + } + switch v[0] { + case "size": + s, err := strconv.ParseUint(v[1], 10, 32) + if err != nil { + return nil, err + } + options.Size = uint32(s) + case "uidmapping": + mapping, err := parseAutoIDMap(v[1], "UID", parentUIDMap) + if err != nil { + return nil, err + } + options.AdditionalUIDMappings = append(options.AdditionalUIDMappings, mapping...) + case "gidmapping": + mapping, err := parseAutoIDMap(v[1], "GID", parentGIDMap) + if err != nil { + return nil, err + } + options.AdditionalGIDMappings = append(options.AdditionalGIDMappings, mapping...) + default: + return nil, fmt.Errorf("unknown option specified: %q", v[0]) + } + } + return &options, nil +} + // ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []string, subUIDMap, subGIDMap string) (*stypes.IDMappingOptions, error) { options := stypes.IDMappingOptions{ @@ -842,7 +998,7 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin options.HostUIDMapping = false options.HostGIDMapping = false options.AutoUserNs = true - opts, err := mode.GetAutoOptions() + opts, err := GetAutoOptions(mode) if err != nil { return nil, err } diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go index 993cea74ec..e3c303a6e9 100644 --- a/pkg/util/utils_test.go +++ b/pkg/util/utils_test.go @@ -391,6 +391,42 @@ func TestParseIDMapUserGroupFlags(t *testing.T) { assert.Equal(t, expectedResultGroup, result) } +func TestParseAutoIDMap(t *testing.T) { + result, err := parseAutoIDMap("3:4:5", "UID", []ruser.IDMap{}) + assert.Equal(t, err, nil) + assert.Equal(t, result, []idtools.IDMap{ + { + ContainerID: 3, + HostID: 4, + Size: 5, + }, + }) +} + +func TestParseAutoIDMapRelative(t *testing.T) { + parentMapping := []ruser.IDMap{ + { + ID: 0, + ParentID: 1000, + Count: 1, + }, + { + ID: 1, + ParentID: 100000, + Count: 65536, + }, + } + result, err := parseAutoIDMap("100:@100000:1", "UID", parentMapping) + assert.Equal(t, err, nil) + assert.Equal(t, result, []idtools.IDMap{ + { + ContainerID: 100, + HostID: 1, + Size: 1, + }, + }) +} + func TestFillIDMap(t *testing.T) { availableRanges := [][2]int{{0, 10}, {10000, 20000}} idmap := []idtools.IDMap{ From 952c7089066f38ace6c1cfff31918d98ebd3a2fa Mon Sep 17 00:00:00 2001 From: kaivol Date: Sun, 10 Dec 2023 14:16:08 +0100 Subject: [PATCH 2/2] added system test Signed-off-by: kaivol --- test/system/170-run-userns.bats | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/system/170-run-userns.bats b/test/system/170-run-userns.bats index 667648e5f7..c4fcac3eda 100644 --- a/test/system/170-run-userns.bats +++ b/test/system/170-run-userns.bats @@ -147,3 +147,12 @@ EOF is "${output}" "$user" "Container should run as the current user" run_podman rmi -f $(pause_image) } + +@test "podman userns=auto with id mapping" { + skip_if_not_rootless + run_podman unshare awk '{if(NR == 2){print $2}}' /proc/self/uid_map + first_id=$output + mapping=1:@$first_id:1 + run_podman run --rm --userns=auto:uidmapping=$mapping $IMAGE awk '{if($1 == 1){print $2}}' /proc/self/uid_map + assert "$output" == 1 +}