Merge pull request #1529 from giuseppe/rootless-improvements

Support multiple users/groups mapped for the rootless case
This commit is contained in:
Michael Crosby 2017-09-11 14:01:31 -04:00 committed by GitHub
commit 8b47a242a9
19 changed files with 408 additions and 128 deletions

View File

@ -22,6 +22,7 @@ RUN apt-get update && apt-get install -y \
protobuf-c-compiler \
protobuf-compiler \
python-minimal \
uidmap \
--no-install-recommends \
&& apt-get clean

View File

@ -77,11 +77,10 @@ localintegration: all
bats -t tests/integration${TESTFLAGS}
rootlessintegration: runcimage
docker run -e TESTFLAGS -t --privileged --rm -v $(CURDIR):/go/src/$(PROJECT) --cap-drop=ALL -u rootless $(RUNC_IMAGE) make localintegration
docker run -e TESTFLAGS -t --privileged --rm -v $(CURDIR):/go/src/$(PROJECT) $(RUNC_IMAGE) make localrootlessintegration
# FIXME: This should not be separate from rootlessintegration's method of running.
localrootlessintegration: all
sudo -u rootless -H PATH="${PATH}" bats -t tests/integration${TESTFLAGS}
tests/rootless.sh
shell: all
docker run -e TESTFLAGS -ti --privileged --rm -v $(CURDIR):/go/src/$(PROJECT) $(RUNC_IMAGE) bash

View File

@ -36,37 +36,27 @@ func (v *ConfigValidator) rootless(config *configs.Config) error {
return nil
}
func rootlessMappings(config *configs.Config) error {
rootuid, err := config.HostRootUID()
if err != nil {
return fmt.Errorf("failed to get root uid from uidMappings: %v", err)
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 rootlessMappings(config *configs.Config) error {
if euid := geteuid(); euid != 0 {
if !config.Namespaces.Contains(configs.NEWUSER) {
return fmt.Errorf("rootless containers require user namespaces")
}
if rootuid != euid {
return fmt.Errorf("rootless containers cannot map container root to a different host user")
}
}
rootgid, err := config.HostRootGID()
if err != nil {
return fmt.Errorf("failed to get root gid from gidMappings: %v", err)
if len(config.UidMappings) == 0 {
return fmt.Errorf("rootless containers requires at least one UID mapping")
}
// Similar to the above test, we need to make sure that we aren't trying to
// map to a group ID that we don't have the right to be.
if rootgid != getegid() {
return fmt.Errorf("rootless containers cannot map container root to a different host group")
}
// We can only map one user and group inside a container (our own).
if len(config.UidMappings) != 1 || config.UidMappings[0].Size != 1 {
return fmt.Errorf("rootless containers cannot map more than one user")
}
if len(config.GidMappings) != 1 || config.GidMappings[0].Size != 1 {
return fmt.Errorf("rootless containers cannot map more than one group")
if len(config.GidMappings) == 0 {
return fmt.Errorf("rootless containers requires at least one UID mapping")
}
return nil
@ -104,11 +94,28 @@ func rootlessMount(config *configs.Config) error {
// Check that the options list doesn't contain any uid= or gid= entries
// that don't resolve to root.
for _, opt := range strings.Split(mount.Data, ",") {
if strings.HasPrefix(opt, "uid=") && opt != "uid=0" {
return fmt.Errorf("cannot specify uid= mount options in rootless containers where argument isn't 0")
if strings.HasPrefix(opt, "uid=") {
var uid int
n, err := fmt.Sscanf(opt, "uid=%d", &uid)
if n != 1 || err != nil {
// Ignore unknown mount options.
continue
}
if !hasIDMapping(uid, config.UidMappings) {
return fmt.Errorf("cannot specify uid= mount options for unmapped uid in rootless containers")
}
}
if strings.HasPrefix(opt, "gid=") && opt != "gid=0" {
return fmt.Errorf("cannot specify gid= mount options in rootless containers where argument isn't 0")
if strings.HasPrefix(opt, "gid=") {
var gid int
n, err := fmt.Sscanf(opt, "gid=%d", &gid)
if n != 1 || err != nil {
// Ignore unknown mount options.
continue
}
if !hasIDMapping(gid, config.GidMappings) {
return fmt.Errorf("cannot specify gid= mount options for unmapped gid in rootless containers")
}
}
}
}

View File

@ -66,28 +66,6 @@ func TestValidateRootlessMappingUid(t *testing.T) {
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if no uid mappings provided")
}
config = rootlessConfig()
config.UidMappings[0].HostID = geteuid() + 1
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if geteuid() != mapped uid")
}
config = rootlessConfig()
config.UidMappings[0].Size = 1024
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if more than one uid mapped")
}
config = rootlessConfig()
config.UidMappings = append(config.UidMappings, configs.IDMap{
HostID: geteuid() + 1,
ContainerID: 0,
Size: 1,
})
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if more than one uid extent mapped")
}
}
func TestValidateRootlessMappingGid(t *testing.T) {
@ -98,28 +76,6 @@ func TestValidateRootlessMappingGid(t *testing.T) {
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if no gid mappings provided")
}
config = rootlessConfig()
config.GidMappings[0].HostID = getegid() + 1
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if getegid() != mapped gid")
}
config = rootlessConfig()
config.GidMappings[0].Size = 1024
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if more than one gid mapped")
}
config = rootlessConfig()
config.GidMappings = append(config.GidMappings, configs.IDMap{
HostID: getegid() + 1,
ContainerID: 0,
Size: 1,
})
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur if more than one gid extent mapped")
}
}
/* rootlessMount() */
@ -149,6 +105,18 @@ func TestValidateRootlessMountUid(t *testing.T) {
if err := validator.Validate(config); err != nil {
t.Errorf("Expected error to not occur when setting uid=0 in mount options: %+v", err)
}
config.Mounts[0].Data = "uid=2"
config.UidMappings[0].Size = 10
if err := validator.Validate(config); err != nil {
t.Errorf("Expected error to not occur when setting uid=2 in mount options and UidMapping[0].size is 10")
}
config.Mounts[0].Data = "uid=20"
config.UidMappings[0].Size = 10
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur when setting uid=20 in mount options and UidMapping[0].size is 10")
}
}
func TestValidateRootlessMountGid(t *testing.T) {
@ -176,6 +144,18 @@ func TestValidateRootlessMountGid(t *testing.T) {
if err := validator.Validate(config); err != nil {
t.Errorf("Expected error to not occur when setting gid=0 in mount options: %+v", err)
}
config.Mounts[0].Data = "gid=5"
config.GidMappings[0].Size = 10
if err := validator.Validate(config); err != nil {
t.Errorf("Expected error to not occur when setting gid=5 in mount options and GidMapping[0].size is 10")
}
config.Mounts[0].Data = "gid=11"
config.GidMappings[0].Size = 10
if err := validator.Validate(config); err == nil {
t.Errorf("Expected error to occur when setting gid=11 in mount options and GidMapping[0].size is 10")
}
}
/* rootlessCgroup() */

View File

@ -44,6 +44,8 @@ type linuxContainer struct {
initProcess parentProcess
initProcessStartTime uint64
criuPath string
newuidmapPath string
newgidmapPath string
m sync.Mutex
criuVersion int
state containerState
@ -1707,6 +1709,12 @@ func (c *linuxContainer) bootstrapData(cloneFlags uintptr, nsMaps map[configs.Na
if !joinExistingUser {
// write uid mappings
if len(c.config.UidMappings) > 0 {
if c.config.Rootless && c.newuidmapPath != "" {
r.AddData(&Bytemsg{
Type: UidmapPathAttr,
Value: []byte(c.newuidmapPath),
})
}
b, err := encodeIDMapping(c.config.UidMappings)
if err != nil {
return nil, err
@ -1727,6 +1735,12 @@ func (c *linuxContainer) bootstrapData(cloneFlags uintptr, nsMaps map[configs.Na
Type: GidmapAttr,
Value: b,
})
if c.config.Rootless && c.newgidmapPath != "" {
r.AddData(&Bytemsg{
Type: GidmapPathAttr,
Value: []byte(c.newgidmapPath),
})
}
// The following only applies if we are root.
if !c.config.Rootless {
// check if we have CAP_SETGID to setgroup properly

View File

@ -140,6 +140,9 @@ func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
}
Cgroupfs(l)
for _, opt := range options {
if opt == nil {
continue
}
if err := opt(l); err != nil {
return nil, err
}
@ -160,6 +163,11 @@ type LinuxFactory struct {
// containers.
CriuPath string
// New{u,g}uidmapPath is the path to the binaries used for mapping with
// rootless containers.
NewuidmapPath string
NewgidmapPath string
// Validator provides validation to container configurations.
Validator validate.Validator
@ -201,6 +209,8 @@ func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, err
config: config,
initArgs: l.InitArgs,
criuPath: l.CriuPath,
newuidmapPath: l.NewuidmapPath,
newgidmapPath: l.NewgidmapPath,
cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
}
c.intelRdtManager = nil
@ -236,6 +246,8 @@ func (l *LinuxFactory) Load(id string) (Container, error) {
config: &state.Config,
initArgs: l.InitArgs,
criuPath: l.CriuPath,
newuidmapPath: l.NewuidmapPath,
newgidmapPath: l.NewgidmapPath,
cgroupManager: l.NewCgroupsManager(state.Config.Cgroups, state.CgroupPaths),
root: containerRoot,
created: state.Created,
@ -349,3 +361,21 @@ func (l *LinuxFactory) validateID(id string) error {
return nil
}
// NewuidmapPath returns an option func to configure a LinuxFactory with the
// provided ..
func NewuidmapPath(newuidmapPath string) func(*LinuxFactory) error {
return func(l *LinuxFactory) error {
l.NewuidmapPath = newuidmapPath
return nil
}
}
// NewgidmapPath returns an option func to configure a LinuxFactory with the
// provided ..
func NewgidmapPath(newgidmapPath string) func(*LinuxFactory) error {
return func(l *LinuxFactory) error {
l.NewgidmapPath = newgidmapPath
return nil
}
}

View File

@ -261,25 +261,27 @@ func setupUser(config *initConfig) error {
}
}
// Rather than just erroring out later in setuid(2) and setgid(2), check
// that the user is mapped here.
if _, err := config.Config.HostUID(int(execUser.Uid)); err != nil {
return fmt.Errorf("cannot set uid to unmapped user in user namespace")
}
if _, err := config.Config.HostGID(int(execUser.Gid)); err != nil {
return fmt.Errorf("cannot set gid to unmapped user in user namespace")
}
if config.Rootless {
if execUser.Uid != 0 {
return fmt.Errorf("cannot run as a non-root user in a rootless container")
}
if execUser.Gid != 0 {
return fmt.Errorf("cannot run as a non-root group in a rootless container")
}
// We cannot set any additional groups in a rootless container and thus we
// bail if the user asked us to do so. TODO: We currently can't do this
// earlier, but if libcontainer.Process.User was typesafe this might work.
// We cannot set any additional groups in a rootless container and thus
// we bail if the user asked us to do so. TODO: We currently can't do
// this check earlier, but if libcontainer.Process.User was typesafe
// this might work.
if len(addGroups) > 0 {
return fmt.Errorf("cannot set any additional groups in a rootless container")
}
}
// before we change to the container's user make sure that the processes STDIO
// is correctly owned by the user that we are switching to.
// Before we change to the container's user make sure that the processes
// STDIO is correctly owned by the user that we are switching to.
if err := fixStdioPermissions(config, execUser); err != nil {
return err
}
@ -298,7 +300,6 @@ func setupUser(config *initConfig) error {
if err := system.Setgid(execUser.Gid); err != nil {
return err
}
if err := system.Setuid(execUser.Uid); err != nil {
return err
}

View File

@ -18,6 +18,8 @@ const (
SetgroupAttr uint16 = 27285
OomScoreAdjAttr uint16 = 27286
RootlessAttr uint16 = 27287
UidmapPathAttr uint16 = 27288
GidmapPathAttr uint16 = 27289
)
type Int32msg struct {

View File

@ -1,3 +1,4 @@
#define _GNU_SOURCE
#include <endian.h>
#include <errno.h>
@ -19,6 +20,8 @@
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <linux/limits.h>
#include <linux/netlink.h>
@ -64,7 +67,13 @@ struct clone_t {
struct nlconfig_t {
char *data;
/* Process settings. */
uint32_t cloneflags;
char *oom_score_adj;
size_t oom_score_adj_len;
/* User namespace settings.*/
char *uidmap;
size_t uidmap_len;
char *gidmap;
@ -72,9 +81,13 @@ struct nlconfig_t {
char *namespaces;
size_t namespaces_len;
uint8_t is_setgroup;
/* Rootless container settings.*/
uint8_t is_rootless;
char *oom_score_adj;
size_t oom_score_adj_len;
char *uidmappath;
size_t uidmappath_len;
char *gidmappath;
size_t gidmappath_len;
};
/*
@ -89,6 +102,8 @@ struct nlconfig_t {
#define SETGROUP_ATTR 27285
#define OOM_SCORE_ADJ_ATTR 27286
#define ROOTLESS_ATTR 27287
#define UIDMAPPATH_ATTR 27288
#define GIDMAPPATH_ATTR 27289
/*
* Use the raw syscall for versions of glibc which don't include a function for
@ -191,22 +206,96 @@ static void update_setgroups(int pid, enum policy_t setgroup)
}
}
static void update_uidmap(int pid, char *map, size_t map_len)
static int try_mapping_tool(const char *app, int pid, char *map, size_t map_len)
{
if (map == NULL || map_len <= 0)
return;
int child;
if (write_file(map, map_len, "/proc/%d/uid_map", pid) < 0)
bail("failed to update /proc/%d/uid_map", pid);
/*
* If @app is NULL, execve will segfault. Just check it here and bail (if
* we're in this path, the caller is already getting desparate and there
* isn't a backup to this failing). This usually would be a configuration
* or programming issue.
*/
if (!app)
bail("mapping tool not present");
child = fork();
if (child < 0)
bail("failed to fork");
if (!child) {
#define MAX_ARGV 20
char *argv[MAX_ARGV];
char *envp[] = {NULL};
char pid_fmt[16];
int argc = 0;
char *next;
snprintf(pid_fmt, 16, "%d", pid);
argv[argc++] = (char *) app;
argv[argc++] = pid_fmt;
/*
* Convert the map string into a list of argument that
* newuidmap/newgidmap can understand.
*/
while (argc < MAX_ARGV) {
if (*map == '\0') {
argv[argc++] = NULL;
break;
}
argv[argc++] = map;
next = strpbrk(map, "\n ");
if (next == NULL)
break;
*next++ = '\0';
map = next + strspn(next, "\n ");
}
execve(app, argv, envp);
bail("failed to execv");
} else {
int status;
while (true) {
if (waitpid(child, &status, 0) < 0) {
if (errno == EINTR)
continue;
bail("failed to waitpid");
}
if (WIFEXITED(status) || WIFSIGNALED(status))
return WEXITSTATUS(status);
}
}
return -1;
}
static void update_gidmap(int pid, char *map, size_t map_len)
static void update_uidmap(const char *path, int pid, char *map, size_t map_len)
{
if (map == NULL || map_len <= 0)
return;
if (write_file(map, map_len, "/proc/%d/gid_map", pid) < 0)
bail("failed to update /proc/%d/gid_map", pid);
if (write_file(map, map_len, "/proc/%d/uid_map", pid) < 0) {
if (errno != EPERM)
bail("failed to update /proc/%d/uid_map", pid);
if (try_mapping_tool(path, pid, map, map_len))
bail("failed to use newuid map on %d", pid);
}
}
static void update_gidmap(const char *path, int pid, char *map, size_t map_len)
{
if (map == NULL || map_len <= 0)
return;
if (write_file(map, map_len, "/proc/%d/gid_map", pid) < 0) {
if (errno != EPERM)
bail("failed to update /proc/%d/gid_map", pid);
if (try_mapping_tool(path, pid, map, map_len))
bail("failed to use newgid map on %d", pid);
}
}
static void update_oom_score_adj(char *data, size_t len)
@ -350,6 +439,14 @@ static void nl_parse(int fd, struct nlconfig_t *config)
config->gidmap = current;
config->gidmap_len = payload_len;
break;
case UIDMAPPATH_ATTR:
config->uidmappath = current;
config->uidmappath_len = payload_len;
break;
case GIDMAPPATH_ATTR:
config->gidmappath = current;
config->gidmappath_len = payload_len;
break;
case SETGROUP_ATTR:
config->is_setgroup = readint8(current);
break;
@ -596,8 +693,8 @@ void nsexec(void)
update_setgroups(child, SETGROUPS_DENY);
/* Set up mappings. */
update_uidmap(child, config.uidmap, config.uidmap_len);
update_gidmap(child, config.gidmap, config.gidmap_len);
update_uidmap(config.uidmappath, child, config.uidmap, config.uidmap_len);
update_gidmap(config.gidmappath, child, config.gidmap, config.gidmap_len);
s = SYNC_USERMAP_ACK;
if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {

11
main.go
View File

@ -61,6 +61,15 @@ func main() {
}
v = append(v, fmt.Sprintf("spec: %s", specs.Version))
app.Version = strings.Join(v, "\n")
root := "/run/runc"
if os.Geteuid() != 0 {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir != "" {
root = runtimeDir + "/runc"
}
}
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug",
@ -78,7 +87,7 @@ func main() {
},
cli.StringFlag{
Name: "root",
Value: "/run/runc",
Value: root,
Usage: "root directory for storage of container state (this should be located in tmpfs)",
},
cli.StringFlag{

View File

@ -50,7 +50,7 @@ value for "bundle" is the current directory.
--debug enable debug output for logging
--log value set the log file path where internal debug information is written (default: "/dev/null")
--log-format value set the format used by logs ('text' (default), or 'json') (default: "text")
--root value root directory for storage of container state (this should be located in tmpfs) (default: "/run/runc")
--root value root directory for storage of container state (this should be located in tmpfs) (default: "/run/runc" or $XDG_RUNTIME_DIR/runc for rootless containers)
--criu value path to the criu binary used for checkpoint and restore (default: "criu")
--systemd-cgroup enable systemd cgroup support, expects cgroupsPath to be of form "slice:prefix:name" for e.g. "system.slice:runc:434234"
--help, -h show help

View File

@ -100,8 +100,8 @@ function teardown() {
}
@test "runc exec --user" {
# --user can't work in rootless containers
requires root
# --user can't work in rootless containers that don't have idmap.
[[ "$ROOTLESS" -ne 0 ]] && requires rootless_idmap
# run busybox detached
runc run -d --console-socket $CONSOLE_SOCKET test_busybox
@ -110,5 +110,5 @@ function teardown() {
runc exec --user 1000:1000 test_busybox id
[ "$status" -eq 0 ]
[[ ${output} == "uid=1000 gid=1000" ]]
[[ "${output}" == "uid=1000 gid=1000"* ]]
}

View File

@ -58,15 +58,41 @@ function __runc() {
"$RUNC" --log /proc/self/fd/2 --root "$ROOT" "$@"
}
# Wrapper for runc spec.
# Wrapper for runc spec, which takes only one argument (the bundle path).
function runc_spec() {
local args=""
! [[ "$#" > 1 ]]
local args=()
local bundle=""
if [ "$ROOTLESS" -ne 0 ]; then
args+="--rootless"
args+=("--rootless")
fi
if [ "$#" -ne 0 ]; then
bundle="$1"
args+=("--bundle" "$bundle")
fi
runc spec $args "$@"
runc spec "${args[@]}"
# Always add additional mappings if we have idmaps.
if [[ "$ROOTLESS" -ne 0 ]] && [[ "$ROOTLESS_FEATURES" == *"idmap"* ]]; then
runc_rootless_idmap "$bundle"
fi
}
# Shortcut to add additional uids and gids, based on the values set as part of
# a rootless configuration.
function runc_rootless_idmap() {
bundle="${1:-.}"
cat "$bundle/config.json" \
| jq '.mounts |= map((select(.type == "devpts") | .options += ["gid=5"]) // .)' \
| jq '.linux.uidMappings |= .+ [{"hostID": '"$ROOTLESS_UIDMAP_START"', "containerID": 1000, "size": '"$ROOTLESS_UIDMAP_LENGTH"'}]' \
| jq '.linux.gidMappings |= .+ [{"hostID": '"$ROOTLESS_GIDMAP_START"', "containerID": 100, "size": 1}]' \
| jq '.linux.gidMappings |= .+ [{"hostID": '"$(($ROOTLESS_GIDMAP_START+10))"', "containerID": 1, "size": 20}]' \
| jq '.linux.gidMappings |= .+ [{"hostID": '"$(($ROOTLESS_GIDMAP_START+100))"', "containerID": 1000, "size": '"$(($ROOTLESS_GIDMAP_LENGTH-1000))"'}]' \
>"$bundle/config.json.tmp"
mv "$bundle/config.json"{.tmp,}
}
# Fails the current test, providing the error given.
@ -90,14 +116,19 @@ function requires() {
skip "test requires ${var}"
fi
;;
rootless_idmap)
if [[ "$ROOTLESS_FEATURES" != *"idmap"* ]]; then
skip "test requires ${var}"
fi
;;
cgroups_kmem)
if [ ! -e "$KMEM" ]; then
skip "Test requires ${var}."
skip "Test requires ${var}"
fi
;;
cgroups_rt)
if [ ! -e "$RT_PERIOD" ]; then
skip "Test requires ${var}."
skip "Test requires ${var}"
fi
;;
*)

View File

@ -51,7 +51,7 @@ function teardown() {
[ ! -e "$HELLO_BUNDLE"/config.json ]
# test generation of spec does not return an error
runc_spec --bundle "$HELLO_BUNDLE"
runc_spec "$HELLO_BUNDLE"
[ "$status" -eq 0 ]
# test generation of spec created our config.json (spec)

View File

@ -21,8 +21,8 @@ function teardown() {
}
@test "runc run detached ({u,g}id != 0)" {
# cannot start containers as another user in rootless setup
requires root
# cannot start containers as another user in rootless setup without idmap
[[ "$ROOTLESS" -ne 0 ]] && requires rootless_idmap
# replace "uid": 0 with "uid": 1000
# and do a similar thing for gid.

View File

@ -21,8 +21,8 @@ function teardown() {
}
@test "runc run ({u,g}id != 0)" {
# cannot start containers as another user in rootless setup
requires root
# cannot start containers as another user in rootless setup without idmap
[[ "$ROOTLESS" -ne 0 ]] && requires rootless_idmap
# replace "uid": 0 with "uid": 1000
# and do a similar thing for gid.

View File

@ -24,9 +24,9 @@ function teardown() {
}
@test "runc run [tty owner]" {
# tty chmod is not doable in rootless containers.
# tty chmod is not doable in rootless containers without idmap.
# TODO: this can be made as a change to the gid test.
requires root
[[ "$ROOTLESS" -ne 0 ]] && requires rootless_idmap
# Replace sh script with stat.
sed -i 's/"sh"/"sh", "-c", "stat -c %u:%g $(tty) | tr : \\\\\\\\n"/' config.json
@ -40,8 +40,8 @@ function teardown() {
}
@test "runc run [tty owner] ({u,g}id != 0)" {
# tty chmod is not doable in rootless containers.
requires root
# tty chmod is not doable in rootless containers without idmap.
[[ "$ROOTLESS" -ne 0 ]] && requires rootless_idmap
# replace "uid": 0 with "uid": 1000
# and do a similar thing for gid.
@ -76,9 +76,9 @@ function teardown() {
}
@test "runc exec [tty owner]" {
# tty chmod is not doable in rootless containers.
# tty chmod is not doable in rootless containers without idmap.
# TODO: this can be made as a change to the gid test.
requires root
[[ "$ROOTLESS" -ne 0 ]] && requires rootless_idmap
# run busybox detached
runc run -d --console-socket $CONSOLE_SOCKET test_busybox
@ -95,8 +95,8 @@ function teardown() {
}
@test "runc exec [tty owner] ({u,g}id != 0)" {
# tty chmod is not doable in rootless containers.
requires root
# tty chmod is not doable in rootless containers without idmap.
[[ "$ROOTLESS" -ne 0 ]] && requires rootless_idmap
# replace "uid": 0 with "uid": 1000
# and do a similar thing for gid.

88
tests/rootless.sh Executable file
View File

@ -0,0 +1,88 @@
#!/bin/bash
# Copyright (C) 2017 SUSE LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# rootless.sh -- Runner for rootless container tests. The purpose of this
# script is to allow for the addition (and testing) of "opportunistic" features
# to rootless containers while still testing the base features. In order to add
# a new feature, please match the existing style. Add an entry to $ALL_FEATURES,
# and add an enable_* and disable_* hook.
ALL_FEATURES=("idmap")
ROOT="$(readlink -f "$(dirname "${BASH_SOURCE}")/..")"
# FEATURE: Opportunistic new{uid,gid}map support, allowing a rootless container
# to be set up with the usage of helper setuid binaries.
function enable_idmap() {
export ROOTLESS_UIDMAP_START=100000 ROOTLESS_UIDMAP_LENGTH=65536
export ROOTLESS_GIDMAP_START=200000 ROOTLESS_GIDMAP_LENGTH=65536
# Set up sub{uid,gid} mappings.
[ -e /etc/subuid.tmp ] && mv /etc/subuid{.tmp,}
( grep -v '^rootless' /etc/subuid ; echo "rootless:$ROOTLESS_UIDMAP_START:$ROOTLESS_UIDMAP_LENGTH" ) > /etc/subuid.tmp
mv /etc/subuid{.tmp,}
[ -e /etc/subgid.tmp ] && mv /etc/subgid{.tmp,}
( grep -v '^rootless' /etc/subgid ; echo "rootless:$ROOTLESS_GIDMAP_START:$ROOTLESS_GIDMAP_LENGTH" ) > /etc/subgid.tmp
mv /etc/subgid{.tmp,}
# Reactivate new{uid,gid}map helpers if applicable.
[ -e /usr/bin/unused-newuidmap ] && mv /usr/bin/{unused-,}newuidmap
[ -e /usr/bin/unused-newgidmap ] && mv /usr/bin/{unused-,}newgidmap
}
function disable_idmap() {
export ROOTLESS_UIDMAP_START ROOTLESS_UIDMAP_LENGTH
export ROOTLESS_GIDMAP_START ROOTLESS_GIDMAP_LENGTH
# Deactivate sub{uid,gid} mappings.
[ -e /etc/subuid ] && mv /etc/subuid{,.tmp}
[ -e /etc/subgid ] && mv /etc/subgid{,.tmp}
# Deactivate new{uid,gid}map helpers. setuid is preserved with mv(1).
[ -e /usr/bin/newuidmap ] && mv /usr/bin/{,unused-}newuidmap
[ -e /usr/bin/newgidmap ] && mv /usr/bin/{,unused-}newgidmap
}
# Create a powerset of $ALL_FEATURES (the set of all subsets of $ALL_FEATURES).
# We test all of the possible combinations (as long as we don't add too many
# feature knobs this shouldn't take too long -- but the number of tested
# combinations is O(2^n)).
function powerset() {
eval printf '%s' $(printf '{,%s+}' "$@"):
}
features_powerset="$(powerset "${ALL_FEATURES[@]}")"
# Iterate over the powerset of all features.
IFS=:
for enabled_features in $features_powerset
do
idx="$(($idx+1))"
echo "[$(printf '%.2d' "$idx")] run rootless tests ... (${enabled_features%%+})"
unset IFS
for feature in "${ALL_FEATURES[@]}"
do
hook_func="disable_$feature"
grep -E "(^|\+)$feature(\+|$)" <<<$enabled_features &>/dev/null && hook_func="enable_$feature"
"$hook_func"
done
# Run the test suite!
set -e
echo path: $PATH
export ROOTLESS_FEATURES="$enabled_features"
sudo -HE -u rootless PATH="$PATH" bats -t "$ROOT/tests/integration"
set +e
done

View File

@ -7,6 +7,7 @@ import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
@ -35,6 +36,9 @@ func loadFactory(context *cli.Context) (libcontainer.Factory, error) {
if err != nil {
return nil, err
}
// We default to cgroupfs, and can only use systemd if the system is a
// systemd box.
cgroupManager := libcontainer.Cgroupfs
if context.GlobalBool("systemd-cgroup") {
if systemd.UseSystemd() {
@ -43,11 +47,28 @@ func loadFactory(context *cli.Context) (libcontainer.Factory, error) {
return nil, fmt.Errorf("systemd cgroup flag passed, but systemd support for managing cgroups is not available")
}
}
if intelrdt.IsEnabled() {
intelRdtManager := libcontainer.IntelRdtFs
return libcontainer.New(abs, cgroupManager, intelRdtManager, libcontainer.CriuPath(context.GlobalString("criu")))
intelRdtManager := libcontainer.IntelRdtFs
if !intelrdt.IsEnabled() {
intelRdtManager = nil
}
return libcontainer.New(abs, cgroupManager, libcontainer.CriuPath(context.GlobalString("criu")))
// We resolve the paths for {newuidmap,newgidmap} from the context of runc,
// to avoid doing a path lookup in the nsexec context. TODO: The binary
// names are not currently configurable.
newuidmap, err := exec.LookPath("newuidmap")
if err != nil {
newuidmap = ""
}
newgidmap, err := exec.LookPath("newgidmap")
if err != nil {
newgidmap = ""
}
return libcontainer.New(abs, cgroupManager, intelRdtManager,
libcontainer.CriuPath(context.GlobalString("criu")),
libcontainer.NewuidmapPath(newuidmap),
libcontainer.NewgidmapPath(newgidmap))
}
// getContainer returns the specified container instance by loading it from state