From bde5b6c1f9f00e6b11832c3b1913af8658a43c76 Mon Sep 17 00:00:00 2001 From: Nalin Dahyabhai Date: Wed, 23 Feb 2022 17:11:56 -0500 Subject: [PATCH] unprivileged: use the right mappings when we don't need to set any When we create a new user namespace so that we can get CAP_SYS_ADMIN over a mount namespace, if we're already UID 0, map the current set of ID mappings over themselves instead of trying to repeat the mapping that's been applied to the container we're in. If we know we've been started in a namespace that doesn't have node-default mappings, log that we know. Don't default to trying metacopy=on with mount_program=fuse-overlayfs, since it knows nothing about the option and all we do is trigger a repeated warning about it not being recognized. Signed-off-by: Nalin Dahyabhai --- cmd/defaults.go | 3 +-- cmd/userns.go | 43 ++++++++++++++++++++++++++++++++++++-- cmd/userns_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 cmd/userns_test.go diff --git a/cmd/defaults.go b/cmd/defaults.go index bb23b48d25..911fb53820 100644 --- a/cmd/defaults.go +++ b/cmd/defaults.go @@ -12,7 +12,7 @@ import ( // Return "chroot" if we know we're not actually root, "oci" otherwise. func builderDefaultIsolation() (string, error) { - if inUserNamespace() { + if inOurUserNamespace() { // We probably don't have enough privileges to use a proper // runtime. // Lean on the container that we're in being itself @@ -50,7 +50,6 @@ func builderDefaultStorage() (string, string, error) { }{ {"overlay", `["mountopt=metacopy=on"]`, nil}, {"overlay", ``, nil}, - {"overlay", `["mount_program=/usr/bin/fuse-overlayfs","mountopt=metacopy=on"]`, builderCanUseOverlayFUSE}, {"overlay", `["mount_program=/usr/bin/fuse-overlayfs"]`, builderCanUseOverlayFUSE}, {"vfs", "", nil}, } { diff --git a/cmd/userns.go b/cmd/userns.go index f70a9aab19..d87518d9bd 100644 --- a/cmd/userns.go +++ b/cmd/userns.go @@ -64,6 +64,13 @@ func parseIDMappings(uidmap, gidmap string) ([]specs.LinuxIDMapping, []specs.Lin if err != nil { klog.Fatalf("Error reading ID mappings for %s: %v\n", uid, err) } + } else { + for i := range UIDs { + UIDs[i].HostID = UIDs[i].ContainerID + } + for i := range GIDs { + GIDs[i].HostID = GIDs[i].ContainerID + } } if uidMappings := parseMapping("uidmap", uidmap); len(uidMappings) != 0 { UIDs = uidMappings @@ -74,13 +81,21 @@ func parseIDMappings(uidmap, gidmap string) ([]specs.LinuxIDMapping, []specs.Lin return UIDs, GIDs } -func inUserNamespace() bool { +// inOurUserNamespace returns true if we've already set up a user namespace of +// our own +func inOurUserNamespace() bool { return os.Getenv(usernsMarkerVariable) != "" } +// isNodeDefaultMapping returns true if the current ID map is the default node +// ID map: one range starting at 0, with length 2^32-1, mapped to itself +func isNodeDefaultMapping(m []specs.LinuxIDMapping) bool { + return len(m) == 1 && m[0].ContainerID == 0 && m[0].HostID == 0 && m[0].Size == 0xffffffff +} + func maybeReexecUsingUserNamespace(uidmap string, useNewuidmap bool, gidmap string, useNewgidmap bool) { // If we've already done all of this, there's no need to do it again. - if inUserNamespace() { + if inOurUserNamespace() { return } @@ -91,6 +106,30 @@ func maybeReexecUsingUserNamespace(uidmap string, useNewuidmap bool, gidmap stri } } + // Log the ID maps we were started with. + UIDs, GIDs, err := unshare.GetHostIDMappings("") + if err != nil { + klog.Fatalf("Error reading current ID mappings: %v\n", err) + } + if !isNodeDefaultMapping(UIDs) || !isNodeDefaultMapping(GIDs) { + uidMap, gidMap := "[", "[" + for i, entry := range UIDs { + if i > 0 { + uidMap += ", " + } + uidMap += fmt.Sprintf("(%d:%d:%d)", entry.ContainerID, entry.HostID, entry.Size) + } + for i, entry := range GIDs { + if i > 0 { + gidMap += ", " + } + gidMap += fmt.Sprintf("(%d:%d:%d)", entry.ContainerID, entry.HostID, entry.Size) + } + uidMap += "]" + gidMap += "]" + klog.V(2).Infof("Started in kernel user namespace with UID map %s and GID map %s.", uidMap, gidMap) + } + // Parse our --uidmap and --gidmap flags into ID mappings and re-exec ourselves. cmd := unshare.Command(append([]string{fmt.Sprintf("%s-in-a-user-namespace", os.Args[0])}, os.Args[1:]...)...) diff --git a/cmd/userns_test.go b/cmd/userns_test.go new file mode 100644 index 0000000000..9ab4e8485e --- /dev/null +++ b/cmd/userns_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "testing" + + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" +) + +func TestIsNodeDefaultMapping(t *testing.T) { + cases := []struct { + description string + mappings []specs.LinuxIDMapping + expected bool + }{ + { + "node defaults", + []specs.LinuxIDMapping{ + {ContainerID: 0, HostID: 0, Size: 0xffffffff}, + }, + true, + }, + { + "typical isolated namespace", + []specs.LinuxIDMapping{ + {ContainerID: 0, HostID: 100100, Size: 65536}, + }, + false, + }, + { + "user with subuid", + []specs.LinuxIDMapping{ + {ContainerID: 0, HostID: 1001, Size: 1}, + {ContainerID: 1, HostID: 100100, Size: 65536}, + }, + false, + }, + { + "multiple ranges", + []specs.LinuxIDMapping{ + {ContainerID: 0, HostID: 1001, Size: 1024}, + {ContainerID: 1024, HostID: 100100, Size: 65536}, + }, + false, + }, + } + for i := range cases { + t.Run(cases[i].description, func(t *testing.T) { + assert.Equalf(t, cases[i].expected, isNodeDefaultMapping(cases[i].mappings), "isNodeDefaultMapping(%v)", cases[i].mappings) + }) + } +}