mirror of
https://github.com/containers/podman.git
synced 2025-08-06 11:32:07 +08:00
rootless: automatically split userns ranges
writing to the id map fails when an extent overlaps multiple mappings in the parent user namespace: $ cat /proc/self/uid_map 0 1000 1 1 100000 65536 $ unshare -U sleep 100 & [1] 1029703 $ printf "0 0 100\n" | tee /proc/$!/uid_map 0 0 100 tee: /proc/1029703/uid_map: Operation not permitted This limitation is particularly annoying when working with rootless containers as each container runs in the rootless user namespace, so a command like: $ podman run --uidmap 0:0:2 --rm fedora echo hi Error: writing file `/proc/664087/gid_map`: Operation not permitted: OCI permission denied would fail since the specified mapping overlaps the first mapping (where the user id is mapped to root) and the second extent with the additional IDs available. Detect such cases and automatically split the specified mapping with the equivalent of: $ podman run --uidmap 0:0:1 --uidmap 1:1:1 --rm fedora echo hi hi A fix has already been proposed for the kernel[1], but even if it accepted it will take time until it is available in a released kernel, so fix it also in pkg/rootless. [1] https://lkml.kernel.org/lkml/20201203150252.1229077-1-gscrivan@redhat.com/ Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
This commit is contained in:
@ -529,6 +529,13 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
|
||||
}
|
||||
}
|
||||
|
||||
availableUIDs, availableGIDs, err := rootless.GetAvailableIDMaps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Config.Linux.UIDMappings = rootless.MaybeSplitMappings(g.Config.Linux.UIDMappings, availableUIDs)
|
||||
g.Config.Linux.GIDMappings = rootless.MaybeSplitMappings(g.Config.Linux.GIDMappings, availableGIDs)
|
||||
|
||||
// Hostname handling:
|
||||
// If we have a UTS namespace, set Hostname in the OCI spec.
|
||||
// Set the HOSTNAME environment variable unless explicitly overridden by
|
||||
@ -536,6 +543,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
|
||||
// set it to the host's hostname instead.
|
||||
hostname := c.Hostname()
|
||||
foundUTS := false
|
||||
|
||||
for _, i := range c.config.Spec.Linux.Namespaces {
|
||||
if i.Type == spec.UTSNamespace && i.Path == "" {
|
||||
foundUTS = true
|
||||
|
@ -2,10 +2,12 @@ package rootless
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/containers/storage"
|
||||
"github.com/opencontainers/runc/libcontainer/user"
|
||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -87,6 +89,20 @@ func GetAvailableGidMap() ([]user.IDMap, error) {
|
||||
return gidMap, gidMapError
|
||||
}
|
||||
|
||||
// GetAvailableIDMaps returns the UID and GID mappings in the
|
||||
// current user namespace.
|
||||
func GetAvailableIDMaps() ([]user.IDMap, []user.IDMap, error) {
|
||||
u, err := GetAvailableUidMap()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
g, err := GetAvailableGidMap()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return u, g, nil
|
||||
}
|
||||
|
||||
func countAvailableIDs(mappings []user.IDMap) int64 {
|
||||
availableUids := int64(0)
|
||||
for _, r := range mappings {
|
||||
@ -116,3 +132,71 @@ func GetAvailableGids() (int64, error) {
|
||||
|
||||
return countAvailableIDs(gids), nil
|
||||
}
|
||||
|
||||
// findIDInMappings find the the mapping that contains the specified ID.
|
||||
// It assumes availableMappings is sorted by ID.
|
||||
func findIDInMappings(id int64, availableMappings []user.IDMap) *user.IDMap {
|
||||
i := sort.Search(len(availableMappings), func(i int) bool {
|
||||
return availableMappings[i].ID >= id
|
||||
})
|
||||
if i < 0 || i >= len(availableMappings) {
|
||||
return nil
|
||||
}
|
||||
r := &availableMappings[i]
|
||||
if id >= r.ID && id < r.ID+r.Count {
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaybeSplitMappings checks whether the specified OCI mappings are possible
|
||||
// in the current user namespace or the specified ranges must be split.
|
||||
func MaybeSplitMappings(mappings []spec.LinuxIDMapping, availableMappings []user.IDMap) []spec.LinuxIDMapping {
|
||||
var ret []spec.LinuxIDMapping
|
||||
var overflow spec.LinuxIDMapping
|
||||
overflow.Size = 0
|
||||
consumed := 0
|
||||
sort.Slice(availableMappings, func(i, j int) bool {
|
||||
return availableMappings[i].ID < availableMappings[j].ID
|
||||
})
|
||||
for {
|
||||
cur := overflow
|
||||
// if there is no overflow left from the previous request, get the next one
|
||||
if cur.Size == 0 {
|
||||
if consumed == len(mappings) {
|
||||
// all done
|
||||
return ret
|
||||
}
|
||||
cur = mappings[consumed]
|
||||
consumed++
|
||||
}
|
||||
|
||||
// Find the range where the first specified ID is present
|
||||
r := findIDInMappings(int64(cur.HostID), availableMappings)
|
||||
if r == nil {
|
||||
// The requested range is not available. Just return the original request
|
||||
// and let other layers deal with it.
|
||||
return mappings
|
||||
}
|
||||
|
||||
offsetInRange := cur.HostID - uint32(r.ID)
|
||||
|
||||
usableIDs := uint32(r.Count) - offsetInRange
|
||||
|
||||
// the current range can satisfy the whole request
|
||||
if usableIDs >= cur.Size {
|
||||
// reset the overflow
|
||||
overflow.Size = 0
|
||||
} else {
|
||||
// the current range can satisfy the request partially
|
||||
// so move the rest to overflow
|
||||
overflow.Size = cur.Size - usableIDs
|
||||
overflow.ContainerID = cur.ContainerID + usableIDs
|
||||
overflow.HostID = cur.HostID + usableIDs
|
||||
|
||||
// and cap to the usableIDs count
|
||||
cur.Size = usableIDs
|
||||
}
|
||||
ret = append(ret, cur)
|
||||
}
|
||||
}
|
||||
|
101
pkg/rootless/rootless_test.go
Normal file
101
pkg/rootless/rootless_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package rootless
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/user"
|
||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
func TestMaybeSplitMappings(t *testing.T) {
|
||||
mappings := []spec.LinuxIDMapping{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: 0,
|
||||
Size: 2,
|
||||
},
|
||||
}
|
||||
desiredMappings := []spec.LinuxIDMapping{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: 0,
|
||||
Size: 1,
|
||||
},
|
||||
{
|
||||
ContainerID: 1,
|
||||
HostID: 1,
|
||||
Size: 1,
|
||||
},
|
||||
}
|
||||
availableMappings := []user.IDMap{
|
||||
{
|
||||
ID: 1,
|
||||
ParentID: 1000000,
|
||||
Count: 65536,
|
||||
},
|
||||
{
|
||||
ID: 0,
|
||||
ParentID: 1000,
|
||||
Count: 1,
|
||||
},
|
||||
}
|
||||
newMappings := MaybeSplitMappings(mappings, availableMappings)
|
||||
if !reflect.DeepEqual(newMappings, desiredMappings) {
|
||||
t.Fatal("wrong mappings generated")
|
||||
}
|
||||
|
||||
mappings = []spec.LinuxIDMapping{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: 0,
|
||||
Size: 2,
|
||||
},
|
||||
}
|
||||
desiredMappings = []spec.LinuxIDMapping{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: 0,
|
||||
Size: 2,
|
||||
},
|
||||
}
|
||||
availableMappings = []user.IDMap{
|
||||
{
|
||||
ID: 0,
|
||||
ParentID: 1000000,
|
||||
Count: 65536,
|
||||
},
|
||||
}
|
||||
newMappings = MaybeSplitMappings(mappings, availableMappings)
|
||||
|
||||
if !reflect.DeepEqual(newMappings, desiredMappings) {
|
||||
t.Fatal("wrong mappings generated")
|
||||
}
|
||||
|
||||
mappings = []spec.LinuxIDMapping{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: 0,
|
||||
Size: 1,
|
||||
},
|
||||
}
|
||||
desiredMappings = []spec.LinuxIDMapping{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: 0,
|
||||
Size: 1,
|
||||
},
|
||||
}
|
||||
availableMappings = []user.IDMap{
|
||||
{
|
||||
ID: 10000,
|
||||
ParentID: 10000,
|
||||
Count: 65536,
|
||||
},
|
||||
}
|
||||
|
||||
newMappings = MaybeSplitMappings(mappings, availableMappings)
|
||||
if !reflect.DeepEqual(newMappings, desiredMappings) {
|
||||
t.Fatal("wrong mappings generated")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user