handling of supplemental groups with userns

From: Mike Frysinger
Date: Tue Sep 22 2015 - 12:22:05 EST


is it possible to map in supplemental groups in a userns when the user
lacks setgid/etc... capabilities in the parent ns ? it doesn't seem
like it's currently possible, but is there a reason to not enable it ?

basically i have a build tool that i want to isolate a bit, but it
requires access to some of my supplemental groups. if i map just
my effective uid/gid, the build will fail when it tries to use the
chown/chgrp commands (gets back EINVAL).

my scenario boils down to:
- normal unprivileged user (uid=8282)
- member of multiple groups (gid=100, getgroups={100,16,250,...})
- create a new userns (to get access to other ns like mount/pid)
but still have access to existing groups where i'm root
- use various features that require caps (new pidns/mntns/etc...)
- create another userns and map back non-root users/groups
i.e. i switch from 8282 to 0, do what i need, then switch back to 8282.

i've attached a simple test program to show the issue. it can map the
current uid/gid fine, but fails to do so with a supplemental group.

my reading of kernel/user_namespace.c shows that it's not possible:
(1) if gid_map has any entries, map_write bails early with EPERM
(2) if gid_map is empty, then writing a single entry (e.g. "0 100 1")
works, but then a 2nd write runs into (1)
(3) if gid_map is empty, then writing multiple entries (e.g.
"0 100 1\n250 250 1\n") fails in new_idmap_permitted -- the first
check is skipped (since new_map->nr_extents is 2), and the user
does not have caps in the parent ns

i'm aware UID_GID_MAP_MAX_EXTENTS is low, so it's not even possible if i
had the caps to map all the existing supplemental groups, which is why i
only want to map the few critical groups. but assuming this use case is
one we want to support, maybe it makes sense to add a knob to map all of
the user's supplemental groups ?

in the mean time, a "quick" fix might be to change new_idmap_permitted
to walk all the extents, and if all the ranges are set to 1, check the
supplemental groups in addition to the current egid ?
-mike
/* To compile:
* gcc test.c
* To run:
* ./a.out [1|2|3|4|5]
* ./a.out -1 "string to write to gid_map"
*/

#define _GNU_SOURCE
#include <assert.h>
#include <err.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
int ret;

FILE *fp;

int uid = getuid();
int gid = getgid();
int nuid = 0;
int ngid = 0;
int suppgid;

gid_t *groups;
size_t i, gsize;

int group_mode;
switch (argc) {
case 0:
case 1:
group_mode = 1;
case 2:
group_mode = atoi(argv[1]);
case 3:
group_mode = -1;
}

/* Find one supplemental group */
gsize = getgroups(0, NULL);
assert(gsize != 0);
groups = malloc(sizeof(*groups) * gsize);
ret = getgroups(gsize, groups);
assert(ret == gsize);
for (i = 0; i < gsize; ++i)
if (groups[i] != gid) {
suppgid = groups[i];
break;
}
if (i == gsize)
errx(1, "could not find a supplemental group to test");
free(groups);

/* Create new userns */
assert(unshare(CLONE_NEWUSER) == 0);

/* Map the current uid */
fp = fopen("/proc/self/uid_map", "we");
assert(fp);
setbuf(fp, NULL);
fprintf(fp, "%i %i 1", nuid, uid);
fclose(fp);

/* Disable setgroups() */
fp = fopen("/proc/self/setgroups", "we");
assert(fp);
setbuf(fp, NULL);
fputs("deny", fp);
fclose(fp);

/* Map the various gids */
fp = fopen("/proc/self/gid_map", "we");
assert(fp);
setbuf(fp, NULL);
switch (group_mode) {
case 1: /* This works */
fprintf(fp, "%i %i 1\n", ngid, gid);
break;
case 2: /* This fails */
fprintf(fp, "%i %i 1\n", ngid, gid);
fprintf(fp, "%i %i 1\n", suppgid, suppgid);
break;
case 3: /* This fails */
fprintf(fp, "%i %i 1\n%i %i 1\n", ngid, gid, suppgid, suppgid);
break;
case 4: /* This fails */
fprintf(fp, "%i %i 1\n", suppgid, suppgid);
break;
case 5: /* This fails */
fprintf(fp, "0 0 10000\n");
break;
case -1:
fprintf(fp, argv[2]);
break;
}
fclose(fp);

/* Validate */
printf("uid:%i gid:%i\n", getuid(), getgid());
gsize = getgroups(0, NULL);
assert(gsize != 0);
groups = malloc(sizeof(*groups) * gsize);
ret = getgroups(gsize, groups);
assert(ret == gsize);
printf("groups: ");
for (i = 0; i < gsize; ++i)
printf("%i ", groups[i]);
printf("\n");
free(groups);
}

Attachment: signature.asc
Description: Digital signature