Repository: containers/podman
Pull request #20827 (2023-11-29 04:21:42)

Support lookup of intermediate ID for uidmapping and gidmapping in `--userns=auto`
In rootless mode, the host IDs in the `uidmapping` and `gidmapping` options of `--userns=auto` are mapped from an intermediate namespace, just like when using the `--uidmap` and `--gidmap` options.
This means that for a given _host ID_, the user first has to look up the _intermediate ID_ manually. 

The `--uidmap` and `--gidmap` options also support prefixing the _host ID_ in the mapping with the `@` symbol, which means that podman will look up the _intermediate ID_ corresponding to the given _host ID_ and it will map the found _intermediate ID_ to the given _container ID_.

This PR adds the functionality mentioned above to the `uidmapping` and `gidmapping` options in `--userns=auto`.


```release-note
`uidmapping` and `gidmapping` options of `--userns=auto` in rootless mode: 
Added the possibility to map actual host IDs (instead of intermediate IDs) by prefixing the host ID 
with the `@` symbol. 
```


openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/containers/podman/pull/20827#" title="Author self-approved">kaivol</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [mtrmac](https://github.com/mtrmac) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=containers%2Fpodman).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/containers/podman/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["mtrmac"]} -->
packit-as-a-service[bot]: Ephemeral COPR build failed. @containers/packit-build please check.
giuseppe: the build is failing

Patch:
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/go.mod b/go.mod
index 7f660bd150..f182deb6ec 100644
--- a/go.mod
+++ b/go.mod
@@ -36,7 +36,7 @@ require (
 	github.com/google/uuid v1.4.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
-	github.com/gorilla/schema v1.2.1
+	github.com/gorilla/schema v1.2.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hugelgupf/p9 v0.3.1-0.20230822151754-54f5c5530921
 	github.com/json-iterator/go v1.1.12
diff --git a/go.sum b/go.sum
index e80530dfd0..a17d41f78e 100644
--- a/go.sum
+++ b/go.sum
@@ -619,8 +619,8 @@ github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
 github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM=
-github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
+github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
+github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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 @<integer>
+	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 @<id>, where <id> 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/vendor/github.com/gorilla/schema/.editorconfig b/vendor/github.com/gorilla/schema/.editorconfig
deleted file mode 100644
index c6b74c3e0d..0000000000
--- a/vendor/github.com/gorilla/schema/.editorconfig
+++ /dev/null
@@ -1,20 +0,0 @@
-; https://editorconfig.org/
-
-root = true
-
-[*]
-insert_final_newline = true
-charset = utf-8
-trim_trailing_whitespace = true
-indent_style = space
-indent_size = 2
-
-[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
-indent_style = tab
-indent_size = 4
-
-[*.md]
-indent_size = 4
-trim_trailing_whitespace = false
-
-eclint_indent_style = unset
\ No newline at end of file
diff --git a/vendor/github.com/gorilla/schema/.gitignore b/vendor/github.com/gorilla/schema/.gitignore
deleted file mode 100644
index 84039fec68..0000000000
--- a/vendor/github.com/gorilla/schema/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-coverage.coverprofile
diff --git a/vendor/github.com/gorilla/schema/LICENSE b/vendor/github.com/gorilla/schema/LICENSE
index bb9d80bc9b..0e5fb87280 100644
--- a/vendor/github.com/gorilla/schema/LICENSE
+++ b/vendor/github.com/gorilla/schema/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2023 The Gorilla Authors. All rights reserved.
+Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
diff --git a/vendor/github.com/gorilla/schema/Makefile b/vendor/github.com/gorilla/schema/Makefile
deleted file mode 100644
index 98f5ab75f9..0000000000
--- a/vendor/github.com/gorilla/schema/Makefile
+++ /dev/null
@@ -1,34 +0,0 @@
-GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '')
-GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest
-
-GO_SEC=$(shell which gosec 2> /dev/null || echo '')
-GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest
-
-GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '')
-GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest
-
-.PHONY: golangci-lint
-golangci-lint:
-	$(if $(GO_LINT), ,go install $(GO_LINT_URI))
-	@echo "##### Running golangci-lint"
-	golangci-lint run -v
-	
-.PHONY: gosec
-gosec:
-	$(if $(GO_SEC), ,go install $(GO_SEC_URI))
-	@echo "##### Running gosec"
-	gosec ./...
-
-.PHONY: govulncheck
-govulncheck:
-	$(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI))
-	@echo "##### Running govulncheck"
-	govulncheck ./...
-
-.PHONY: verify
-verify: golangci-lint gosec govulncheck
-
-.PHONY: test
-test:
-	@echo "##### Running tests"
-	go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./...
\ No newline at end of file
diff --git a/vendor/github.com/gorilla/schema/README.md b/vendor/github.com/gorilla/schema/README.md
index dbeff3d0c8..aefdd66996 100644
--- a/vendor/github.com/gorilla/schema/README.md
+++ b/vendor/github.com/gorilla/schema/README.md
@@ -1,12 +1,8 @@
-# gorilla/schema
+schema
+======
+[![GoDoc](https://godoc.org/github.com/gorilla/schema?status.svg)](https://godoc.org/github.com/gorilla/schema) [![Build Status](https://travis-ci.org/gorilla/schema.png?branch=master)](https://travis-ci.org/gorilla/schema)
+[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/schema/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/schema?badge)
 
-![testing](https://github.com/gorilla/schema/actions/workflows/test.yml/badge.svg)
-[![codecov](https://codecov.io/github/gorilla/schema/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/schema)
-[![godoc](https://godoc.org/github.com/gorilla/schema?status.svg)](https://godoc.org/github.com/gorilla/schema)
-[![sourcegraph](https://sourcegraph.com/github.com/gorilla/schema/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/schema?badge)
-
-
-![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5)
 
 Package gorilla/schema converts structs to and from form values.
 
diff --git a/vendor/github.com/gorilla/schema/cache.go b/vendor/github.com/gorilla/schema/cache.go
index bf21697cf1..0746c1202c 100644
--- a/vendor/github.com/gorilla/schema/cache.go
+++ b/vendor/github.com/gorilla/schema/cache.go
@@ -12,7 +12,7 @@ import (
 	"sync"
 )
 
-var errInvalidPath = errors.New("schema: invalid path")
+var invalidPath = errors.New("schema: invalid path")
 
 // newCache returns a new cache.
 func newCache() *cache {
@@ -53,13 +53,13 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
 	keys := strings.Split(p, ".")
 	for i := 0; i < len(keys); i++ {
 		if t.Kind() != reflect.Struct {
-			return nil, errInvalidPath
+			return nil, invalidPath
 		}
 		if struc = c.get(t); struc == nil {
-			return nil, errInvalidPath
+			return nil, invalidPath
 		}
 		if field = struc.get(keys[i]); field == nil {
-			return nil, errInvalidPath
+			return nil, invalidPath
 		}
 		// Valid field. Append index.
 		path = append(path, field.name)
@@ -72,10 +72,10 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
 			// So checking i+2 is not necessary anymore.
 			i++
 			if i+1 > len(keys) {
-				return nil, errInvalidPath
+				return nil, invalidPath
 			}
 			if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil {
-				return nil, errInvalidPath
+				return nil, invalidPath
 			}
 			parts = append(parts, pathPart{
 				path:  path,
diff --git a/vendor/github.com/gorilla/schema/decoder.go b/vendor/github.com/gorilla/schema/decoder.go
index 28b560bbbb..025e438b56 100644
--- a/vendor/github.com/gorilla/schema/decoder.go
+++ b/vendor/github.com/gorilla/schema/decoder.go
@@ -193,7 +193,7 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values
 		if v.Type().Kind() == reflect.Struct {
 			for i := 0; i < v.NumField(); i++ {
 				field := v.Field(i)
-				if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous {
+				if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous == true {
 					field.Set(reflect.New(field.Type().Elem()))
 				}
 			}
diff --git a/vendor/github.com/gorilla/schema/encoder.go b/vendor/github.com/gorilla/schema/encoder.go
index 51f0a78ca4..f0ed631210 100644
--- a/vendor/github.com/gorilla/schema/encoder.go
+++ b/vendor/github.com/gorilla/schema/encoder.go
@@ -3,7 +3,6 @@ package schema
 import (
 	"errors"
 	"fmt"
-	"log"
 	"reflect"
 	"strconv"
 )
@@ -94,11 +93,8 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
 		}
 
 		// Encode struct pointer types if the field is a valid pointer and a struct.
-		if isValidStructPointer(v.Field(i)) && !e.hasCustomEncoder(v.Field(i).Type()) {
-			err := e.encode(v.Field(i).Elem(), dst)
-			if err != nil {
-				log.Fatal(err)
-			}
+		if isValidStructPointer(v.Field(i)) {
+			e.encode(v.Field(i).Elem(), dst)
 			continue
 		}
 
@@ -116,10 +112,7 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
 		}
 
 		if v.Field(i).Type().Kind() == reflect.Struct {
-			err := e.encode(v.Field(i), dst)
-			if err != nil {
-				log.Fatal(err)
-			}
+			e.encode(v.Field(i), dst)
 			continue
 		}
 
@@ -149,11 +142,6 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
 	return nil
 }
 
-func (e *Encoder) hasCustomEncoder(t reflect.Type) bool {
-	_, exists := e.regenc[t]
-	return exists
-}
-
 func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc {
 	if f, ok := reg[t]; ok {
 		return f
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a348c03d9e..efef50e37c 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -664,8 +664,8 @@ github.com/gorilla/handlers
 # github.com/gorilla/mux v1.8.1
 ## explicit; go 1.20
 github.com/gorilla/mux
-# github.com/gorilla/schema v1.2.1
-## explicit; go 1.20
+# github.com/gorilla/schema v1.2.0
+## explicit
 github.com/gorilla/schema
 # github.com/hashicorp/errwrap v1.1.0
 ## explicit


Test patch:
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{
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
+}