mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-28 11:25:21 +01:00
886 lines
22 KiB
C
886 lines
22 KiB
C
/*-
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*
|
|
* Copyright(c) 2024 Baptiste Daroussin <[email protected]>
|
|
* Copyright (c) 2025 Kushagra Srivastava <[email protected]>
|
|
* Copyright (c) 2025 The FreeBSD Foundation
|
|
*
|
|
* Portions of this software were developed by Olivier Certner
|
|
* <[email protected]> at Kumacom SARL under sponsorship from the FreeBSD
|
|
* Foundation.
|
|
*/
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/errno.h>
|
|
#include <sys/limits.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,
|
|
};
|
|
|
|
/*
|
|
* The default value should cover almost all cases.
|
|
*
|
|
* For getpwnam_r(), we assume:
|
|
* - 88 bytes for 'struct passwd'
|
|
* - Less than 16 bytes for the user name
|
|
* - A typical shadow hash of 106 bytes
|
|
* - Less than 16 bytes for the login class name
|
|
* - Less than 64 bytes for GECOS info
|
|
* - Less than 128 bytes for the home directory
|
|
* - Less than 32 bytes for the shell path
|
|
* Total: 256 + 88 + 106 = 450.
|
|
*
|
|
* For getgrnam_r(), we assume:
|
|
* - 32 bytes for 'struct group'
|
|
* - Less than 16 bytes for the group name
|
|
* - Some hash of 106 bytes
|
|
* - No more than 16 members, each of less than 16 bytes (=> 256 bytes)
|
|
* Total: 256 + 32 + 16 + 106 = 410.
|
|
*
|
|
* We thus choose 512 (leeway, power of 2).
|
|
*/
|
|
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
|
|
/*
|
|
* We never allocate more than a page at a time when reaching
|
|
* a page (except perhaps for the first increment, up to two).
|
|
* Use roundup2() to be immune to previous cases' changes. */
|
|
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);
|
|
};
|
|
|
|
/*
|
|
* Wraps functions needing a backing allocation.
|
|
*
|
|
* Uses 'alloc' as the starting allocation, and may extend it as necessary.
|
|
* 'alloc' is never freed, even on failure of the wrapped function.
|
|
*
|
|
* The function is expected to return ERANGE if and only if the provided
|
|
* allocation is not big enough. All other values are passed through.
|
|
*/
|
|
static int
|
|
alloc_wrap(struct alloc_wrap_data *const data, struct alloc *alloc)
|
|
{
|
|
int error;
|
|
|
|
/* Avoid a systematic ERANGE on first iteration. */
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* Wraps getpwnam_r(), automatically dealing with memory allocation.
|
|
*
|
|
* 'alloc' may be any allocation (even empty), and will be extended as
|
|
* necessary. It is not freed on error.
|
|
*
|
|
* On success, '*pwdp' is filled with a pointer to the returned 'struct passwd',
|
|
* and on failure, is set to NULL.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* Wraps getgrnam_r(), automatically dealing with memory allocation.
|
|
*
|
|
* 'alloc' may be any allocation (even empty), and will be extended as
|
|
* necessary. It is not freed on error.
|
|
*
|
|
* On success, '*grpp' is filled with a pointer to the returned 'struct group',
|
|
* and on failure, is set to NULL.
|
|
*/
|
|
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));
|
|
}
|
|
|
|
/*
|
|
* Retrieve the UID from a user string.
|
|
*
|
|
* Tries first to interpret the string as a user name, then as a numeric ID
|
|
* (this order is prescribed by POSIX for a number of utilities).
|
|
*
|
|
* 'pwdp' and 'allocp' must be NULL or non-NULL together. If non-NULL, then
|
|
* 'allocp' can be any allocation (possibly empty) and will be extended to
|
|
* contain the result if necessary. It will not be freed (even on failure).
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/* See parse_user_pwd() for the doc. */
|
|
static uid_t
|
|
parse_user(const char *s)
|
|
{
|
|
return (parse_user_pwd(s, NULL, NULL));
|
|
}
|
|
|
|
/*
|
|
* Retrieve the GID from a group string.
|
|
*
|
|
* Tries first to interpret the string as a group name, then as a numeric ID
|
|
* (this order is prescribed by POSIX for a number of utilities).
|
|
*/
|
|
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];
|
|
}
|
|
|
|
/*
|
|
* Remove elements in 'set' that are in 'remove'.
|
|
*
|
|
* Expects both arrays to have been treated with sort_uniq_groups(). Works in
|
|
* O(n + m), modifying 'set' in place.
|
|
*/
|
|
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)
|
|
/* Nothing to remove. */
|
|
return;
|
|
|
|
cand = set->groups[0];
|
|
to_rem = remove->groups[0];
|
|
|
|
for (;;) {
|
|
if (cand < to_rem) {
|
|
/* Keep. */
|
|
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]; /* No duplicates. */
|
|
if (rem == remove->nb)
|
|
break;
|
|
} else {
|
|
to_rem = remove->groups[++rem];
|
|
if (rem == remove->nb)
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* All remaining groups in 'set' must be kept. */
|
|
if (from == to)
|
|
/* Nothing was removed. 'set' will stay the same. */
|
|
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; /* '-u' or '-k' needed. */
|
|
|
|
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;
|
|
|
|
|
|
/*
|
|
* Process options.
|
|
*/
|
|
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;
|
|
|
|
/*
|
|
* Determine users.
|
|
*
|
|
* We do that first as in some cases we need to retrieve the
|
|
* corresponding password database entry to be able to set the primary
|
|
* groups.
|
|
*/
|
|
|
|
if (start_from_current_users) {
|
|
if (user_name != NULL)
|
|
errx(EXIT_FAILURE, "-k incompatible with -u");
|
|
|
|
/*
|
|
* If starting from the current user(s) as a base, finding one
|
|
* of them in the password database and using its groups would
|
|
* be quite surprising, so we instead let '-k' imply '-i'.
|
|
*/
|
|
start_from_current_groups = true;
|
|
} else {
|
|
uid_t uid;
|
|
|
|
/*
|
|
* In the case of any overrides, we impose an explicit base user
|
|
* via '-u' or '-k' instead of implicitly taking 'root' as the
|
|
* base.
|
|
*/
|
|
if (user_name == NULL) {
|
|
if (need_user)
|
|
errx(EXIT_FAILURE,
|
|
"Some overrides specified, "
|
|
"'-u' or '-k' needed.");
|
|
user_name = default_user;
|
|
}
|
|
|
|
/*
|
|
* Even if all user overrides are present as well as primary and
|
|
* supplementary groups ones, in which case the final result
|
|
* doesn't depend on '-u', we still call parse_user_pwd() to
|
|
* check that the passed username is correct.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* Determine primary groups.
|
|
*/
|
|
|
|
/*
|
|
* When not starting from the current groups, we need to set all
|
|
* primary groups. If '-g' was not passed, we use the primary
|
|
* group from the password database as the "base" to which
|
|
* overrides '--egid', '--rgid' and '--svgid' apply. But if all
|
|
* overrides were specified, we in fact just don't need the
|
|
* password database at all.
|
|
*
|
|
* '-g' is treated outside of this 'if' as it can also be used
|
|
* as an override.
|
|
*/
|
|
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) {
|
|
/*
|
|
* We always call parse_group() even in case all overrides are
|
|
* present to check that the passed group is valid.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* Determine supplementary groups.
|
|
*/
|
|
|
|
/*
|
|
* This makes sense to catch user's mistakes. It is not a strong
|
|
* limitation of the code below (allowing this case is just a matter of,
|
|
* in the block treating '-s' with '@' below, replacing an assert() by
|
|
* a reset of 'supp_groups').
|
|
*/
|
|
if (supp_groups_str != NULL && supp_mod_str != NULL &&
|
|
supp_mod_str[0] == '@')
|
|
errx(EXIT_FAILURE, "'-G' and '-s' with '@' are incompatible");
|
|
|
|
/*
|
|
* Determine the supplementary groups to start with, but only if we
|
|
* really need to operate on them later (and set them back).
|
|
*/
|
|
if (!start_from_current_groups) {
|
|
assert(!start_from_current_users);
|
|
|
|
if (supp_groups_str == NULL && (supp_mod_str == NULL ||
|
|
supp_mod_str[0] != '@')) {
|
|
/*
|
|
* If we are to replace supplementary groups (i.e.,
|
|
* neither '-i' nor '-k' was specified) and they are not
|
|
* completely specified (with '-g' or '-s' with '@'), we
|
|
* start from those in the groups database if we were
|
|
* passed a user name that is in the password database
|
|
* (this is a protection against erroneous ID/name
|
|
* conflation in the groups database), else we simply
|
|
* error.
|
|
*/
|
|
|
|
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);
|
|
|
|
/*
|
|
* Have to set SETCREDF_SUPP_GROUPS here since we may be
|
|
* in the case where neither '-G' nor '-s' was passed,
|
|
* but we still have to set the supplementary groups to
|
|
* those of the groups database.
|
|
*/
|
|
setcred_flags |= SETCREDF_SUPP_GROUPS;
|
|
}
|
|
} else if (supp_groups_str == NULL && supp_mod_str != NULL &&
|
|
supp_mod_str[0] != '@') {
|
|
/*
|
|
* We do not need to determine the current groups if, as for the
|
|
* '!start_from_current_groups' case, we are going to replace
|
|
* them entirely, but here also if we do not amend them at all
|
|
* (because they are by definition already in place).
|
|
*/
|
|
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");
|
|
}
|
|
|
|
/*
|
|
* Setting SETCREDF_SUPP_GROUPS here is not necessary, we will
|
|
* do it below since 'supp_mod_str' != NULL.
|
|
*/
|
|
}
|
|
|
|
if (supp_groups_str != NULL) {
|
|
char *p = supp_groups_str;
|
|
char *tok;
|
|
|
|
/*
|
|
* We will set the supplementary groups to exactly the set
|
|
* passed with '-G', and we took care above not to retrieve
|
|
* "base" groups (current ones or those from the groups
|
|
* database) in this case.
|
|
*/
|
|
assert(group_array_is_empty(&supp_groups));
|
|
|
|
/* WARNING: 'supp_groups_str' going to be modified. */
|
|
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;
|
|
|
|
/* WARNING: 'supp_mod_str' going to be modified. */
|
|
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");
|
|
/* See same assert() above. */
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* We don't need to pass the kernel a normalized representation of the
|
|
* new supplementary groups set (array sorted and without duplicates),
|
|
* so we don't do it here unless we need to remove some groups, where it
|
|
* enables more efficient algorithms (if the number of groups for some
|
|
* reason grows out of control).
|
|
*/
|
|
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()");
|
|
|
|
/*
|
|
* We don't bother freeing memory still allocated at this point as we
|
|
* are about to exec() or exit.
|
|
*/
|
|
|
|
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");
|
|
}
|