Skip to content

Commit

Permalink
Merge pull request #4144 from cyphar/1.1-ns-path-handling
Browse files Browse the repository at this point in the history
[1.1] *: fix several issues with userns path handling
  • Loading branch information
lifubang committed Dec 16, 2023
2 parents 75d99b4 + 617db78 commit 930fde5
Show file tree
Hide file tree
Showing 13 changed files with 538 additions and 39 deletions.
6 changes: 3 additions & 3 deletions libcontainer/configs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ type Rlimit struct {

// IDMap represents UID/GID Mappings for User Namespaces.
type IDMap struct {
ContainerID int `json:"container_id"`
HostID int `json:"host_id"`
Size int `json:"size"`
ContainerID int64 `json:"container_id"`
HostID int64 `json:"host_id"`
Size int64 `json:"size"`
}

// Seccomp represents syscall restrictions
Expand Down
30 changes: 24 additions & 6 deletions libcontainer/configs/config_linux.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package configs

import "errors"
import (
"errors"
"fmt"
"math"
)

var (
errNoUIDMap = errors.New("User namespaces enabled, but no uid mappings found.")
Expand All @@ -16,11 +20,18 @@ func (c Config) HostUID(containerId int) (int, error) {
if c.UidMappings == nil {
return -1, errNoUIDMap
}
id, found := c.hostIDFromMapping(containerId, c.UidMappings)
id, found := c.hostIDFromMapping(int64(containerId), c.UidMappings)
if !found {
return -1, errNoUserMap
}
return id, nil
// If we are a 32-bit binary running on a 64-bit system, it's possible
// the mapped user is too large to store in an int, which means we
// cannot do the mapping. We can't just return an int64, because
// os.Setuid() takes an int.
if id > math.MaxInt {
return -1, fmt.Errorf("mapping for uid %d (host id %d) is larger than native integer size (%d)", containerId, id, math.MaxInt)
}
return int(id), nil
}
// Return unchanged id.
return containerId, nil
Expand All @@ -39,11 +50,18 @@ func (c Config) HostGID(containerId int) (int, error) {
if c.GidMappings == nil {
return -1, errNoGIDMap
}
id, found := c.hostIDFromMapping(containerId, c.GidMappings)
id, found := c.hostIDFromMapping(int64(containerId), c.GidMappings)
if !found {
return -1, errNoGroupMap
}
return id, nil
// If we are a 32-bit binary running on a 64-bit system, it's possible
// the mapped user is too large to store in an int, which means we
// cannot do the mapping. We can't just return an int64, because
// os.Setgid() takes an int.
if id > math.MaxInt {
return -1, fmt.Errorf("mapping for gid %d (host id %d) is larger than native integer size (%d)", containerId, id, math.MaxInt)
}
return int(id), nil
}
// Return unchanged id.
return containerId, nil
Expand All @@ -57,7 +75,7 @@ func (c Config) HostRootGID() (int, error) {

// Utility function that gets a host ID for a container ID from user namespace map
// if that ID is present in the map.
func (c Config) hostIDFromMapping(containerID int, uMap []IDMap) (int, bool) {
func (c Config) hostIDFromMapping(containerID int64, uMap []IDMap) (int64, bool) {
for _, m := range uMap {
if (containerID >= m.ContainerID) && (containerID <= (m.ContainerID + m.Size - 1)) {
hostID := m.HostID + (containerID - m.ContainerID)
Expand Down
31 changes: 12 additions & 19 deletions libcontainer/configs/validate/rootless.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,18 @@ func (v *ConfigValidator) rootlessEUID(config *configs.Config) error {
return nil
}

func hasIDMapping(id int, mappings []configs.IDMap) bool {
for _, m := range mappings {
if id >= m.ContainerID && id < m.ContainerID+m.Size {
return true
}
}
return false
}

func rootlessEUIDMappings(config *configs.Config) error {
if !config.Namespaces.Contains(configs.NEWUSER) {
return errors.New("rootless container requires user namespaces")
}

if len(config.UidMappings) == 0 {
return errors.New("rootless containers requires at least one UID mapping")
}
if len(config.GidMappings) == 0 {
return errors.New("rootless containers requires at least one GID mapping")
// We only require mappings if we are not joining another userns.
if path := config.Namespaces.PathOf(configs.NEWUSER); path == "" {
if len(config.UidMappings) == 0 {
return errors.New("rootless containers requires at least one UID mapping")
}
if len(config.GidMappings) == 0 {
return errors.New("rootless containers requires at least one GID mapping")
}
}
return nil
}
Expand All @@ -70,8 +63,8 @@ func rootlessEUIDMount(config *configs.Config) error {
// Ignore unknown mount options.
continue
}
if !hasIDMapping(uid, config.UidMappings) {
return errors.New("cannot specify uid= mount options for unmapped uid in rootless containers")
if _, err := config.HostUID(uid); err != nil {
return fmt.Errorf("cannot specify uid=%d mount option for rootless container: %w", uid, err)
}
}

Expand All @@ -82,8 +75,8 @@ func rootlessEUIDMount(config *configs.Config) error {
// Ignore unknown mount options.
continue
}
if !hasIDMapping(gid, config.GidMappings) {
return errors.New("cannot specify gid= mount options for unmapped gid in rootless containers")
if _, err := config.HostGID(gid); err != nil {
return fmt.Errorf("cannot specify gid=%d mount option for rootless container: %w", gid, err)
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions libcontainer/configs/validate/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,19 @@ func (v *ConfigValidator) security(config *configs.Config) error {
func (v *ConfigValidator) usernamespace(config *configs.Config) error {
if config.Namespaces.Contains(configs.NEWUSER) {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
return errors.New("USER namespaces aren't enabled in the kernel")
return errors.New("user namespaces aren't enabled in the kernel")
}
hasPath := config.Namespaces.PathOf(configs.NEWUSER) != ""
hasMappings := config.UidMappings != nil || config.GidMappings != nil
if !hasPath && !hasMappings {
return errors.New("user namespaces enabled, but no namespace path to join nor mappings to apply specified")
}
// The hasPath && hasMappings validation case is handled in specconv --
// we cache the mappings in Config during specconv in the hasPath case,
// so we cannot do that validation here.
} else {
if config.UidMappings != nil || config.GidMappings != nil {
return errors.New("User namespace mappings specified, but USER namespace isn't enabled in the config")
return errors.New("user namespace mappings specified, but user namespace isn't enabled in the config")
}
}
return nil
Expand Down
10 changes: 6 additions & 4 deletions libcontainer/configs/validate/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func TestValidateSecurityWithoutNEWNS(t *testing.T) {
}
}

func TestValidateUsernamespace(t *testing.T) {
func TestValidateUserNamespace(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
t.Skip("Test requires userns.")
}
Expand All @@ -161,6 +161,8 @@ func TestValidateUsernamespace(t *testing.T) {
{Type: configs.NEWUSER},
},
),
UidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
GidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
}

validator := New()
Expand All @@ -170,11 +172,11 @@ func TestValidateUsernamespace(t *testing.T) {
}
}

func TestValidateUsernamespaceWithoutUserNS(t *testing.T) {
uidMap := configs.IDMap{ContainerID: 123}
func TestValidateUsernsMappingWithoutNamespace(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
UidMappings: []configs.IDMap{uidMap},
UidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
GidMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
}

validator := New()
Expand Down
2 changes: 1 addition & 1 deletion libcontainer/container_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -2268,7 +2268,7 @@ func ignoreTerminateErrors(err error) error {

func requiresRootOrMappingTool(c *configs.Config) bool {
gidMap := []configs.IDMap{
{ContainerID: 0, HostID: os.Getegid(), Size: 1},
{ContainerID: 0, HostID: int64(os.Getegid()), Size: 1},
}
return !reflect.DeepEqual(c.GidMappings, gidMap)
}
6 changes: 6 additions & 0 deletions libcontainer/integration/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/userns"
"github.com/opencontainers/runtime-spec/specs-go"

"golang.org/x/sys/unix"
Expand Down Expand Up @@ -1588,6 +1589,11 @@ func TestInitJoinNetworkAndUser(t *testing.T) {
config2 := newTemplateConfig(t, &tParam{userns: true})
config2.Namespaces.Add(configs.NEWNET, netns1)
config2.Namespaces.Add(configs.NEWUSER, userns1)
// Emulate specconv.setupUserNamespace().
uidMap, gidMap, err := userns.GetUserNamespaceMappings(userns1)
ok(t, err)
config2.UidMappings = uidMap
config2.GidMappings = gidMap
config2.Cgroups.Path = "integration/test2"
container2, err := newContainer(t, config2)
ok(t, err)
Expand Down
41 changes: 38 additions & 3 deletions libcontainer/specconv/spec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/devices"
"github.com/opencontainers/runc/libcontainer/seccomp"
"github.com/opencontainers/runc/libcontainer/userns"
libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -926,9 +927,9 @@ next:
func setupUserNamespace(spec *specs.Spec, config *configs.Config) error {
create := func(m specs.LinuxIDMapping) configs.IDMap {
return configs.IDMap{
HostID: int(m.HostID),
ContainerID: int(m.ContainerID),
Size: int(m.Size),
HostID: int64(m.HostID),
ContainerID: int64(m.ContainerID),
Size: int64(m.Size),
}
}
if spec.Linux != nil {
Expand All @@ -939,6 +940,40 @@ func setupUserNamespace(spec *specs.Spec, config *configs.Config) error {
config.GidMappings = append(config.GidMappings, create(m))
}
}
if path := config.Namespaces.PathOf(configs.NEWUSER); path != "" {
// Cache the current userns mappings in our configuration, so that we
// can calculate uid and gid mappings within runc. These mappings are
// never used for configuring the container if the path is set.
uidMap, gidMap, err := userns.GetUserNamespaceMappings(path)
if err != nil {
return fmt.Errorf("failed to cache mappings for userns: %w", err)
}
// We cannot allow uid or gid mappings to be set if we are also asked
// to join a userns.
if config.UidMappings != nil || config.GidMappings != nil {
// FIXME: It turns out that containerd and CRIO pass both a userns
// path and the mappings of the namespace in the same config.json.
// Such a configuration is technically not valid, but we used to
// require mappings be specified, and thus users worked around our
// bug -- so we can't regress it at the moment. But we also don't
// want to produce broken behaviour if the mapping doesn't match
// the userns. So (for now) we output a warning if the actual
// userns mappings match the configuration, otherwise we return an
// error.
if !userns.IsSameMapping(uidMap, config.UidMappings) ||
!userns.IsSameMapping(gidMap, config.GidMappings) {
return errors.New("user namespaces enabled, but both namespace path and non-matching mapping specified -- you may only provide one")
}
logrus.Warnf("config.json has both a userns path to join and a matching userns mapping specified -- you may only provide one. Future versions of runc may return an error with this configuration, please report a bug on <https://github.com/opencontainers/runc> if you see this warning and cannot update your configuration.")
}

config.UidMappings = uidMap
config.GidMappings = gidMap
logrus.WithFields(logrus.Fields{
"uid_map": uidMap,
"gid_map": gidMap,
}).Debugf("config uses path-based userns configuration -- current uid and gid mappings cached")
}
rootUID, err := config.HostRootUID()
if err != nil {
return err
Expand Down
34 changes: 34 additions & 0 deletions libcontainer/specconv/spec_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,40 @@ func TestDupNamespaces(t *testing.T) {
}
}

func TestUserNamespaceMappingAndPath(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
t.Skip("Test requires userns.")
}

spec := &specs.Spec{
Root: &specs.Root{
Path: "rootfs",
},
Linux: &specs.Linux{
UIDMappings: []specs.LinuxIDMapping{
{ContainerID: 0, HostID: 1000, Size: 1000},
},
GIDMappings: []specs.LinuxIDMapping{
{ContainerID: 0, HostID: 2000, Size: 1000},
},
Namespaces: []specs.LinuxNamespace{
{
Type: "user",
Path: "/proc/1/ns/user",
},
},
},
}

_, err := CreateLibcontainerConfig(&CreateOpts{
Spec: spec,
})

if !strings.Contains(err.Error(), "both namespace path and non-matching mapping specified") {
t.Errorf("user namespace with path and non-matching mapping should be forbidden, got error %v", err)
}
}

func TestNonZeroEUIDCompatibleSpecconvValidate(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
t.Skip("Test requires userns.")
Expand Down
79 changes: 79 additions & 0 deletions libcontainer/userns/userns_maps.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <stdlib.h>

/*
* All of the code here is run inside an aync-signal-safe context, so we need
* to be careful to not call any functions that could cause issues. In theory,
* since we are a Go program, there are fewer restrictions in practice, it's
* better to be safe than sorry.
*
* The only exception is exit, which we need to call to make sure we don't
* return into runc.
*/

void bail(int pipefd, const char *fmt, ...)
{
va_list args;

va_start(args, fmt);
vdprintf(pipefd, fmt, args);
va_end(args);

exit(1);
}

int spawn_userns_cat(char *userns_path, char *path, int outfd, int errfd)
{
char buffer[4096] = { 0 };

pid_t child = fork();
if (child != 0)
return child;
/* in child */

/* Join the target userns. */
int nsfd = open(userns_path, O_RDONLY);
if (nsfd < 0)
bail(errfd, "open userns path %s failed: %m", userns_path);

int err = setns(nsfd, CLONE_NEWUSER);
if (err < 0)
bail(errfd, "setns %s failed: %m", userns_path);

close(nsfd);

/* Pipe the requested file contents. */
int fd = open(path, O_RDONLY);
if (fd < 0)
bail(errfd, "open %s in userns %s failed: %m", path, userns_path);

int nread, ntotal = 0;
while ((nread = read(fd, buffer, sizeof(buffer))) != 0) {
if (nread < 0)
bail(errfd, "read bytes from %s failed (after %d total bytes read): %m", path, ntotal);
ntotal += nread;

int nwritten = 0;
while (nwritten < nread) {
int n = write(outfd, buffer, nread - nwritten);
if (n < 0)
bail(errfd, "write %d bytes from %s failed (after %d bytes written): %m",
nread - nwritten, path, nwritten);
nwritten += n;
}
if (nread != nwritten)
bail(errfd, "mismatch for bytes read and written: %d read != %d written", nread, nwritten);
}

close(fd);
close(outfd);
close(errfd);

/* We must exit here, otherwise we would return into a forked runc. */
exit(0);
}
Loading

0 comments on commit 930fde5

Please sign in to comment.