Skip to content

Commit

Permalink
Merge pull request #20827 from kaivol/userns-auto-intermediate-id-lookup
Browse files Browse the repository at this point in the history
Support lookup of intermediate ID for uidmapping and gidmapping in `--userns=auto`
  • Loading branch information
openshift-merge-bot[bot] authored Dec 11, 2023
2 parents c87311b + 952c708 commit 611ba2f
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 46 deletions.
4 changes: 4 additions & 0 deletions docs/source/markdown/options/userns.container.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pkg/domain/infra/runtime_libpod.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
44 changes: 0 additions & 44 deletions pkg/namespaces/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"fmt"
"strconv"
"strings"

"github.com/containers/storage/types"
)

const (
Expand Down Expand Up @@ -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) {
Expand Down
158 changes: 157 additions & 1 deletion pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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
}
Expand Down
36 changes: 36 additions & 0 deletions pkg/util/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
9 changes: 9 additions & 0 deletions test/system/170-run-userns.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 611ba2f

Please sign in to comment.