#include <sys/errno.h>
#include <sys/limits.h>
#include <sys/types.h>
#include <sys/ucred.h>
#include <assert.h>
#include <err.h>
#include <getopt.h>
#include <grp.h>
#include <paths.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void
usage(void)
{
fprintf(stderr,
"Usage: mdo [options] [--] [command [args...]]\n"
"\n"
"Options:\n"
" -u <user> Target user (name or UID; name sets groups)\n"
" -k Keep current user, allows selective overrides "
"(implies -i)\n"
" -i Keep current groups, unless explicitly overridden\n"
" -g <group> Override primary group (name or GID)\n"
" -G <g1,g2,...> Set supplementary groups (name or GID list)\n"
" -s <mods> Modify supplementary groups using:\n"
" @ (first) to reset, +group to add, -group to remove\n"
"\n"
"Advanced UID/GID overrides:\n"
" --euid <uid> Set effective UID\n"
" --ruid <uid> Set real UID\n"
" --svuid <uid> Set saved UID\n"
" --egid <gid> Set effective GID\n"
" --rgid <gid> Set real GID\n"
" --svgid <gid> Set saved GID\n"
"\n"
" -h Show this help message\n"
"\n"
"Examples:\n"
" mdo -u alice id\n"
" mdo -u 1001 -g wheel -G staff,operator sh\n"
" mdo -u bob -s +wheel,+operator id\n"
" mdo -k --ruid 1002 --egid 1004 id\n"
);
exit(1);
}
struct alloc {
void *start;
size_t size;
};
static const struct alloc ALLOC_INITIALIZER = {
.start = NULL,
.size = 0,
};
static const size_t ALLOC_FIRST_SIZE = 512;
static bool
alloc_is_empty(const struct alloc *const alloc)
{
if (alloc->size == 0) {
assert(alloc->start == NULL);
return (true);
} else {
assert(alloc->start != NULL);
return (false);
}
}
static void
alloc_realloc(struct alloc *const alloc)
{
const size_t old_size = alloc->size;
size_t new_size;
if (old_size == 0) {
assert(alloc->start == NULL);
new_size = ALLOC_FIRST_SIZE;
} else if (old_size < PAGE_SIZE)
new_size = 2 * old_size;
else
new_size = roundup2(old_size, PAGE_SIZE) + PAGE_SIZE;
alloc->start = realloc(alloc->start, new_size);
if (alloc->start == NULL)
errx(EXIT_FAILURE,
"cannot realloc allocation (old size: %zu, new: %zu)",
old_size, new_size);
alloc->size = new_size;
}
static void
alloc_free(struct alloc *const alloc)
{
if (!alloc_is_empty(alloc)) {
free(alloc->start);
*alloc = ALLOC_INITIALIZER;
}
}
struct alloc_wrap_data {
int (*func)(void *data, const struct alloc *alloc);
};
static int
alloc_wrap(struct alloc_wrap_data *const data, struct alloc *alloc)
{
int error;
if (alloc_is_empty(alloc))
alloc_realloc(alloc);
for (;;) {
error = data->func(data, alloc);
if (error != ERANGE)
break;
alloc_realloc(alloc);
}
return (error);
}
struct getpwnam_wrapper_data {
struct alloc_wrap_data wrapped;
const char *name;
struct passwd **pwdp;
};
static int
wrapped_getpwnam_r(void *data, const struct alloc *alloc)
{
struct passwd *const pwd = alloc->start;
struct passwd *result;
struct getpwnam_wrapper_data *d = data;
int error;
assert(alloc->size >= sizeof(*pwd));
error = getpwnam_r(d->name, pwd, (char *)(pwd + 1),
alloc->size - sizeof(*pwd), &result);
if (error == 0) {
if (result == NULL)
error = ENOENT;
} else
assert(result == NULL);
*d->pwdp = result;
return (error);
}
static int
alloc_getpwnam(const char *name, struct passwd **pwdp,
struct alloc *const alloc)
{
struct getpwnam_wrapper_data data;
data.wrapped.func = wrapped_getpwnam_r;
data.name = name;
data.pwdp = pwdp;
return (alloc_wrap((struct alloc_wrap_data *)&data, alloc));
}
struct getgrnam_wrapper_data {
struct alloc_wrap_data wrapped;
const char *name;
struct group **grpp;
};
static int
wrapped_getgrnam_r(void *data, const struct alloc *alloc)
{
struct group *grp = alloc->start;
struct group *result;
struct getgrnam_wrapper_data *d = data;
int error;
assert(alloc->size >= sizeof(*grp));
error = getgrnam_r(d->name, grp, (char *)(grp + 1),
alloc->size - sizeof(*grp), &result);
if (error == 0) {
if (result == NULL)
error = ENOENT;
} else
assert(result == NULL);
*d->grpp = result;
return (error);
}
static int
alloc_getgrnam(const char *const name, struct group **const grpp,
struct alloc *const alloc)
{
struct getgrnam_wrapper_data data;
data.wrapped.func = wrapped_getgrnam_r;
data.name = name;
data.grpp = grpp;
return (alloc_wrap((struct alloc_wrap_data *)&data, alloc));
}
static uid_t
parse_user_pwd(const char *s, struct passwd **pwdp, struct alloc *allocp)
{
struct passwd *pwd;
struct alloc alloc = ALLOC_INITIALIZER;
const char *errp;
uid_t uid;
int error;
assert((pwdp == NULL && allocp == NULL) ||
(pwdp != NULL && allocp != NULL));
if (pwdp == NULL) {
pwdp = &pwd;
allocp = &alloc;
}
error = alloc_getpwnam(s, pwdp, allocp);
if (error == 0) {
uid = (*pwdp)->pw_uid;
goto finish;
} else if (error != ENOENT)
errc(EXIT_FAILURE, error,
"cannot access the password database");
uid = strtonum(s, 0, UID_MAX, &errp);
if (errp != NULL)
errx(EXIT_FAILURE, "invalid UID '%s': %s", s, errp);
finish:
if (allocp == &alloc)
alloc_free(allocp);
return (uid);
}
static uid_t
parse_user(const char *s)
{
return (parse_user_pwd(s, NULL, NULL));
}
static gid_t
parse_group(const char *s)
{
struct group *grp;
struct alloc alloc = ALLOC_INITIALIZER;
const char *errp;
gid_t gid;
int error;
error = alloc_getgrnam(s, &grp, &alloc);
if (error == 0) {
gid = grp->gr_gid;
goto finish;
} else if (error != ENOENT)
errc(EXIT_FAILURE, error, "cannot access the group database");
gid = strtonum(s, 0, GID_MAX, &errp);
if (errp != NULL)
errx(EXIT_FAILURE, "invalid GID '%s': %s", s, errp);
finish:
alloc_free(&alloc);
return (gid);
}
struct group_array {
u_int nb;
gid_t *groups;
};
static const struct group_array GROUP_ARRAY_INITIALIZER = {
.nb = 0,
.groups = NULL,
};
static bool
group_array_is_empty(const struct group_array *const ga)
{
return (ga->nb == 0);
}
static void
realloc_groups(struct group_array *const ga, const u_int diff)
{
const u_int new_nb = ga->nb + diff;
const size_t new_size = new_nb * sizeof(*ga->groups);
assert(new_nb >= diff && new_size >= new_nb);
ga->groups = realloc(ga->groups, new_size);
if (ga->groups == NULL)
err(EXIT_FAILURE, "realloc of groups failed");
ga->nb = new_nb;
}
static int
gidp_cmp(const void *p1, const void *p2)
{
const gid_t g1 = *(const gid_t *)p1;
const gid_t g2 = *(const gid_t *)p2;
return ((g1 > g2) - (g1 < g2));
}
static void
sort_uniq_groups(struct group_array *const ga)
{
size_t j = 0;
if (ga->nb <= 1)
return;
qsort(ga->groups, ga->nb, sizeof(gid_t), gidp_cmp);
for (size_t i = 1; i < ga->nb; ++i)
if (ga->groups[i] != ga->groups[j])
ga->groups[++j] = ga->groups[i];
}
static void
remove_groups(struct group_array *const set,
const struct group_array *const remove)
{
u_int from = 0, to = 0, rem = 0;
gid_t cand, to_rem;
if (set->nb == 0 || remove->nb == 0)
return;
cand = set->groups[0];
to_rem = remove->groups[0];
for (;;) {
if (cand < to_rem) {
if (to != from)
set->groups[to] = cand;
++to;
cand = set->groups[++from];
if (from == set->nb)
break;
} else if (cand == to_rem) {
cand = set->groups[++from];
if (from == set->nb)
break;
to_rem = remove->groups[++rem];
if (rem == remove->nb)
break;
} else {
to_rem = remove->groups[++rem];
if (rem == remove->nb)
break;
}
}
if (from == to)
return;
memmove(set->groups + to, set->groups + from,
(set->nb - from) * sizeof(gid_t));
set->nb = to + (set->nb - from);
}
int
main(int argc, char **argv)
{
const char *const default_user = "root";
const char *user_name = NULL;
const char *primary_group = NULL;
char *supp_groups_str = NULL;
char *supp_mod_str = NULL;
bool start_from_current_groups = false;
bool start_from_current_users = false;
const char *euid_str = NULL;
const char *ruid_str = NULL;
const char *svuid_str = NULL;
const char *egid_str = NULL;
const char *rgid_str = NULL;
const char *svgid_str = NULL;
bool need_user = false;
const int go_euid = 1000;
const int go_ruid = 1001;
const int go_svuid = 1002;
const int go_egid = 1003;
const int go_rgid = 1004;
const int go_svgid = 1005;
const struct option longopts[] = {
{"euid", required_argument, NULL, go_euid},
{"ruid", required_argument, NULL, go_ruid},
{"svuid", required_argument, NULL, go_svuid},
{"egid", required_argument, NULL, go_egid},
{"rgid", required_argument, NULL, go_rgid},
{"svgid", required_argument, NULL, go_svgid},
{NULL, 0, NULL, 0}
};
int ch;
struct setcred wcred = SETCRED_INITIALIZER;
u_int setcred_flags = 0;
struct passwd *pw = NULL;
struct alloc pw_alloc = ALLOC_INITIALIZER;
struct group_array supp_groups = GROUP_ARRAY_INITIALIZER;
struct group_array supp_rem = GROUP_ARRAY_INITIALIZER;
while (ch = getopt_long(argc, argv, "+G:g:hiks:u:", longopts, NULL),
ch != -1) {
switch (ch) {
case 'G':
supp_groups_str = optarg;
need_user = true;
break;
case 'g':
primary_group = optarg;
need_user = true;
break;
case 'h':
usage();
case 'i':
start_from_current_groups = true;
break;
case 'k':
start_from_current_users = true;
break;
case 's':
supp_mod_str = optarg;
need_user = true;
break;
case 'u':
user_name = optarg;
break;
case go_euid:
euid_str = optarg;
need_user = true;
break;
case go_ruid:
ruid_str = optarg;
need_user = true;
break;
case go_svuid:
svuid_str = optarg;
need_user = true;
break;
case go_egid:
egid_str = optarg;
need_user = true;
break;
case go_rgid:
rgid_str = optarg;
need_user = true;
break;
case go_svgid:
svgid_str = optarg;
need_user = true;
break;
default:
usage();
}
}
argc -= optind;
argv += optind;
if (start_from_current_users) {
if (user_name != NULL)
errx(EXIT_FAILURE, "-k incompatible with -u");
start_from_current_groups = true;
} else {
uid_t uid;
if (user_name == NULL) {
if (need_user)
errx(EXIT_FAILURE,
"Some overrides specified, "
"'-u' or '-k' needed.");
user_name = default_user;
}
uid = parse_user_pwd(user_name, &pw, &pw_alloc);
wcred.sc_uid = wcred.sc_ruid = wcred.sc_svuid = uid;
setcred_flags |= SETCREDF_UID | SETCREDF_RUID |
SETCREDF_SVUID;
}
if (euid_str != NULL) {
wcred.sc_uid = parse_user(euid_str);
setcred_flags |= SETCREDF_UID;
}
if (ruid_str != NULL) {
wcred.sc_ruid = parse_user(ruid_str);
setcred_flags |= SETCREDF_RUID;
}
if (svuid_str != NULL) {
wcred.sc_svuid = parse_user(svuid_str);
setcred_flags |= SETCREDF_SVUID;
}
if (!start_from_current_groups && primary_group == NULL &&
(egid_str == NULL || rgid_str == NULL || svgid_str == NULL)) {
if (pw == NULL)
errx(EXIT_FAILURE,
"must specify primary groups or a user name "
"with an entry in the password database");
wcred.sc_gid = wcred.sc_rgid = wcred.sc_svgid =
pw->pw_gid;
setcred_flags |= SETCREDF_GID | SETCREDF_RGID |
SETCREDF_SVGID;
}
if (primary_group != NULL) {
wcred.sc_gid = wcred.sc_rgid = wcred.sc_svgid =
parse_group(primary_group);
setcred_flags |= SETCREDF_GID | SETCREDF_RGID | SETCREDF_SVGID;
}
if (egid_str != NULL) {
wcred.sc_gid = parse_group(egid_str);
setcred_flags |= SETCREDF_GID;
}
if (rgid_str != NULL) {
wcred.sc_rgid = parse_group(rgid_str);
setcred_flags |= SETCREDF_RGID;
}
if (svgid_str != NULL) {
wcred.sc_svgid = parse_group(svgid_str);
setcred_flags |= SETCREDF_SVGID;
}
if (supp_groups_str != NULL && supp_mod_str != NULL &&
supp_mod_str[0] == '@')
errx(EXIT_FAILURE, "'-G' and '-s' with '@' are incompatible");
if (!start_from_current_groups) {
assert(!start_from_current_users);
if (supp_groups_str == NULL && (supp_mod_str == NULL ||
supp_mod_str[0] != '@')) {
if (pw == NULL)
errx(EXIT_FAILURE,
"must specify the full supplementary "
"groups set or a user name with an entry "
"in the password database");
const long ngroups_alloc = sysconf(_SC_NGROUPS_MAX) + 1;
gid_t *groups;
int ngroups;
groups = malloc(sizeof(*groups) * ngroups_alloc);
if (groups == NULL)
errx(EXIT_FAILURE,
"cannot allocate memory to retrieve "
"user groups from the groups database");
ngroups = ngroups_alloc;
getgrouplist(user_name, pw->pw_gid, groups, &ngroups);
if (ngroups > ngroups_alloc)
err(EXIT_FAILURE,
"too many groups for user '%s'",
user_name);
realloc_groups(&supp_groups, ngroups);
memcpy(supp_groups.groups + supp_groups.nb - ngroups,
groups, ngroups * sizeof(*groups));
free(groups);
setcred_flags |= SETCREDF_SUPP_GROUPS;
}
} else if (supp_groups_str == NULL && supp_mod_str != NULL &&
supp_mod_str[0] != '@') {
const int ngroups = getgroups(0, NULL);
if (ngroups > 0) {
realloc_groups(&supp_groups, ngroups);
if (getgroups(ngroups, supp_groups.groups +
supp_groups.nb - ngroups) < 0)
err(EXIT_FAILURE, "getgroups() failed");
}
}
if (supp_groups_str != NULL) {
char *p = supp_groups_str;
char *tok;
assert(group_array_is_empty(&supp_groups));
while ((tok = strsep(&p, ",")) != NULL) {
gid_t g;
if (*tok == '\0')
continue;
g = parse_group(tok);
realloc_groups(&supp_groups, 1);
supp_groups.groups[supp_groups.nb - 1] = g;
}
setcred_flags |= SETCREDF_SUPP_GROUPS;
}
if (supp_mod_str != NULL) {
char *p = supp_mod_str;
char *tok;
gid_t gid;
while ((tok = strsep(&p, ",")) != NULL) {
switch (tok[0]) {
case '\0':
break;
case '@':
if (tok != supp_mod_str)
errx(EXIT_FAILURE, "'@' must be "
"the first token in '-s' option");
assert(group_array_is_empty(&supp_groups));
break;
case '+':
case '-':
gid = parse_group(tok + 1);
if (tok[0] == '+') {
realloc_groups(&supp_groups, 1);
supp_groups.groups[supp_groups.nb - 1] = gid;
} else {
realloc_groups(&supp_rem, 1);
supp_rem.groups[supp_rem.nb - 1] = gid;
}
break;
default:
errx(EXIT_FAILURE,
"invalid '-s' token '%s' at index %zu",
tok, tok - supp_mod_str);
}
}
setcred_flags |= SETCREDF_SUPP_GROUPS;
}
if (!group_array_is_empty(&supp_groups) &&
!group_array_is_empty(&supp_rem)) {
sort_uniq_groups(&supp_groups);
sort_uniq_groups(&supp_rem);
remove_groups(&supp_groups, &supp_rem);
}
if ((setcred_flags & SETCREDF_SUPP_GROUPS) != 0) {
wcred.sc_supp_groups = supp_groups.groups;
wcred.sc_supp_groups_nb = supp_groups.nb;
}
if (setcred(setcred_flags, &wcred, sizeof(wcred)) != 0)
err(EXIT_FAILURE, "setcred()");
if (*argv == NULL) {
const char *sh = getenv("SHELL");
if (sh == NULL)
sh = _PATH_BSHELL;
execlp(sh, sh, "-i", NULL);
} else {
execvp(argv[0], argv);
}
err(EXIT_FAILURE, "exec failed");
}