tmpfiles: add new 'r' line type to add UIDs/GIDs to the pool to allocate UIDs/GIDs from

This way we can guarantee a limited amount of compatibility with
login.defs, by generate an appopriate "r" line out of it, on package
installation.
This commit is contained in:
Lennart Poettering 2014-08-19 19:05:11 +02:00
parent 81163121e6
commit 8530dc4467
7 changed files with 512 additions and 78 deletions

1
.gitignore vendored
View File

@ -228,6 +228,7 @@
/test-time
/test-tmpfiles
/test-udev
/test-uid-range
/test-unifont
/test-unit-file
/test-unit-name

View File

@ -862,6 +862,8 @@ libsystemd_shared_la_SOURCES = \
src/shared/base-filesystem.h \
src/shared/memfd.c \
src/shared/memfd.h \
src/shared/uid-range.c \
src/shared/uid-range.h \
src/shared/nss-util.h
nodist_libsystemd_shared_la_SOURCES = \
@ -1322,7 +1324,8 @@ tests += \
test-capability \
test-async \
test-ratelimit \
test-condition-util
test-condition-util \
test-uid-range
EXTRA_DIST += \
test/a.service \
@ -1487,6 +1490,12 @@ test_util_SOURCES = \
test_util_LDADD = \
libsystemd-core.la
test_uid_range_SOURCES = \
src/test/test-uid-range.c
test_uid_range_LDADD = \
libsystemd-core.la
test_socket_util_SOURCES = \
src/test/test-socket-util.c

View File

@ -134,6 +134,25 @@ u root 0 "Superuser" /root</programlisting>
will be implicitly
created.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>r</varname></term>
<listitem><para>Add a range of
numeric UIDs/GIDs to the pool
to allocate new UIDs and GIDs
from. If no line of this type
is specified the range of
UIDs/GIDs is set to some
compiled-in default. Note that
both UIDs and GIDs are
allocated from the same pool,
in order to ensure that users
and groups of the same name
are likely to carry the same
numeric UID and
GID.</para></listitem>
</varlistentry>
</variablelist>
</refsect2>
@ -154,6 +173,10 @@ u root 0 "Superuser" /root</programlisting>
<para>For <varname>m</varname> lines this
field should contain the user name to add to a
group.</para>
<para>For lines of type <varname>r</varname>
this field should be set to
<literal>-</literal>.</para>
</refsect2>
<refsect2>
@ -175,6 +198,14 @@ u root 0 "Superuser" /root</programlisting>
<para>For <varname>m</varname> lines this
field should contain the group name to add to
a user to.</para>
<para>For lines of type <varname>r</varname>
this field should be set to a UID/GID range in
the format <literal>FROM-TO</literal> where
both values are formatted as decimal ASCII
numbers. Alternatively, a single UID/GID may
be specified formatted as decimal ASCII
numbers.</para>
</refsect2>
<refsect2>

205
src/shared/uid-range.c Normal file
View File

@ -0,0 +1,205 @@
/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
/***
This file is part of systemd.
Copyright 2014 Lennart Poettering
systemd is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
systemd is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/
#include "util.h"
#include "uid-range.h"
static bool uid_range_intersect(UidRange *range, uid_t start, uid_t nr) {
assert(range);
return range->start <= start + nr &&
range->start + range->nr >= start;
}
static void uid_range_coalesce(UidRange **p, unsigned *n) {
unsigned i, j;
assert(p);
assert(n);
for (i = 0; i < *n; i++) {
for (j = i + 1; j < *n; j++) {
UidRange *x = (*p)+i, *y = (*p)+j;
if (uid_range_intersect(x, y->start, y->nr)) {
uid_t begin, end;
begin = MIN(x->start, y->start);
end = MAX(x->start + x->nr, y->start + y->nr);
x->start = begin;
x->nr = end - begin;
if (*n > j+1)
memmove(y, y+1, sizeof(UidRange) * (*n - j -1));
(*n) --;
j--;
}
}
}
}
static int uid_range_compare(const void *a, const void *b) {
const UidRange *x = a, *y = b;
if (x->start < y->start)
return -1;
if (x->start > y->start)
return 1;
if (x->nr < y->nr)
return -1;
if (x->nr > y->nr)
return 1;
return 0;
}
int uid_range_add(UidRange **p, unsigned *n, uid_t start, uid_t nr) {
bool found = false;
UidRange *x;
unsigned i;
assert(p);
assert(n);
if (nr <= 0)
return 0;
for (i = 0; i < *n; i++) {
x = (*p) + i;
if (uid_range_intersect(x, start, nr)) {
found = true;
break;
}
}
if (found) {
uid_t begin, end;
begin = MIN(x->start, start);
end = MAX(x->start + x->nr, start + nr);
x->start = begin;
x->nr = end - begin;
} else {
UidRange *t;
t = realloc(*p, sizeof(UidRange) * (*n + 1));
if (!t)
return -ENOMEM;
*p = t;
x = t + ((*n) ++);
x->start = start;
x->nr = nr;
}
qsort(*p, *n, sizeof(UidRange), uid_range_compare);
uid_range_coalesce(p, n);
return *n;
}
int uid_range_add_str(UidRange **p, unsigned *n, const char *s) {
uid_t start, nr;
const char *t;
int r;
assert(p);
assert(n);
assert(s);
t = strchr(s, '-');
if (t) {
char *b;
uid_t end;
b = strndupa(s, t - s);
r = parse_uid(b, &start);
if (r < 0)
return r;
r = parse_uid(t+1, &end);
if (r < 0)
return r;
if (end < start)
return -EINVAL;
nr = end - start + 1;
} else {
r = parse_uid(s, &start);
if (r < 0)
return r;
nr = 1;
}
return uid_range_add(p, n, start, nr);
}
int uid_range_next_lower(const UidRange *p, unsigned n, uid_t *uid) {
uid_t closest = (uid_t) -1, candidate;
unsigned i;
assert(p);
assert(uid);
candidate = *uid - 1;
for (i = 0; i < n; i++) {
uid_t begin, end;
begin = p[i].start;
end = p[i].start + p[i].nr - 1;
if (candidate >= begin && candidate <= end) {
*uid = candidate;
return 1;
}
if (end < candidate)
closest = end;
}
if (closest == (uid_t) -1)
return -EBUSY;
*uid = closest;
return 1;
}
bool uid_range_contains(const UidRange *p, unsigned n, uid_t uid) {
unsigned i;
assert(p);
assert(uid);
for (i = 0; i < n; i++)
if (uid >= p[i].start && uid < p[i].start + p[i].nr)
return true;
return false;
}

34
src/shared/uid-range.h Normal file
View File

@ -0,0 +1,34 @@
/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
#pragma once
/***
This file is part of systemd.
Copyright 2014 Lennart Poettering
systemd is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
systemd is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/
#include <sys/types.h>
typedef struct UidRange {
uid_t start, nr;
} UidRange;
int uid_range_add(UidRange **p, unsigned *n, uid_t start, uid_t nr);
int uid_range_add_str(UidRange **p, unsigned *n, const char *s);
int uid_range_next_lower(const UidRange *p, unsigned n, uid_t *uid);
bool uid_range_contains(const UidRange *p, unsigned n, uid_t uid);

View File

@ -38,11 +38,13 @@
#include "utf8.h"
#include "label.h"
#include "fileio-label.h"
#include "uid-range.h"
typedef enum ItemType {
ADD_USER = 'u',
ADD_GROUP = 'g',
ADD_MEMBER = 'm',
ADD_RANGE = 'r',
} ItemType;
typedef struct Item {
ItemType type;
@ -82,8 +84,9 @@ static Hashmap *members = NULL;
static Hashmap *database_uid = NULL, *database_user = NULL;
static Hashmap *database_gid = NULL, *database_group = NULL;
static uid_t search_uid = SYSTEM_UID_MAX;
static gid_t search_gid = SYSTEM_GID_MAX;
static uid_t search_uid = (uid_t) -1;
static UidRange *uid_range = NULL;
static unsigned n_uid_range = 0;
#define UID_TO_PTR(u) (ULONG_TO_PTR(u+1))
#define PTR_TO_UID(u) ((uid_t) (PTR_TO_ULONG(u)-1))
@ -916,7 +919,7 @@ static int add_user(Item *i) {
if (read_id_from_file(i, &c, NULL) > 0) {
if (c <= 0 || c > SYSTEM_UID_MAX)
if (c <= 0 || !uid_range_contains(uid_range, n_uid_range, c))
log_debug("User ID " UID_FMT " of file not suitable for %s.", c, i->name);
else {
r = uid_is_ok(c, i->name);
@ -947,7 +950,12 @@ static int add_user(Item *i) {
/* And if that didn't work either, let's try to find a free one */
if (!i->uid_set) {
for (; search_uid > 0; search_uid--) {
for (;;) {
r = uid_range_next_lower(uid_range, n_uid_range, &search_uid);
if (r < 0) {
log_error("No free user ID available for %s.", i->name);
return r;
}
r = uid_is_ok(search_uid, i->name);
if (r < 0) {
@ -957,15 +965,8 @@ static int add_user(Item *i) {
break;
}
if (search_uid <= 0) {
log_error("No free user ID available for %s.", i->name);
return -E2BIG;
}
i->uid_set = true;
i->uid = search_uid;
search_uid--;
}
r = hashmap_ensure_allocated(&todo_uids, trivial_hash_func, trivial_compare_func);
@ -1083,7 +1084,7 @@ static int add_group(Item *i) {
if (read_id_from_file(i, NULL, &c) > 0) {
if (c <= 0 || c > SYSTEM_GID_MAX)
if (c <= 0 || !uid_range_contains(uid_range, n_uid_range, c))
log_debug("Group ID " GID_FMT " of file not suitable for %s.", c, i->name);
else {
r = gid_is_ok(c);
@ -1101,8 +1102,15 @@ static int add_group(Item *i) {
/* And if that didn't work either, let's try to find a free one */
if (!i->gid_set) {
for (; search_gid > 0; search_gid--) {
r = gid_is_ok(search_gid);
for (;;) {
/* We look for new GIDs in the UID pool! */
r = uid_range_next_lower(uid_range, n_uid_range, &search_uid);
if (r < 0) {
log_error("No free group ID available for %s.", i->name);
return r;
}
r = gid_is_ok(search_uid);
if (r < 0) {
log_error("Failed to verify gid " GID_FMT ": %s", i->gid, strerror(-r));
return r;
@ -1110,15 +1118,8 @@ static int add_group(Item *i) {
break;
}
if (search_gid <= 0) {
log_error("No free group ID available for %s.", i->name);
return -E2BIG;
}
i->gid_set = true;
i->gid = search_gid;
search_gid--;
i->gid = search_uid;
}
r = hashmap_ensure_allocated(&todo_gids, trivial_hash_func, trivial_compare_func);
@ -1384,7 +1385,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
{}
};
_cleanup_free_ char *action = NULL, *name = NULL, *id = NULL, *resolved_name = NULL, *description = NULL, *home = NULL;
_cleanup_free_ char *action = NULL, *name = NULL, *id = NULL, *resolved_name = NULL, *resolved_id = NULL, *description = NULL, *home = NULL;
_cleanup_(item_freep) Item *i = NULL;
Item *existing;
Hashmap *h;
@ -1417,46 +1418,119 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
return -EINVAL;
}
if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER)) {
if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER, ADD_RANGE)) {
log_error("[%s:%u] Unknown command command type '%c'.", fname, line, action[0]);
return -EBADMSG;
}
/* Verify name */
r = specifier_printf(name, specifier_table, NULL, &resolved_name);
if (r < 0) {
log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name);
return r;
if (isempty(name) || streq(name, "-")) {
free(name);
name = NULL;
}
if (!valid_user_group_name(resolved_name)) {
log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_name);
return -EINVAL;
if (name) {
r = specifier_printf(name, specifier_table, NULL, &resolved_name);
if (r < 0) {
log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name);
return r;
}
if (!valid_user_group_name(resolved_name)) {
log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_name);
return -EINVAL;
}
}
/* Simplify remaining columns */
/* Verify id */
if (isempty(id) || streq(id, "-")) {
free(id);
id = NULL;
}
if (id) {
r = specifier_printf(id, specifier_table, NULL, &resolved_id);
if (r < 0) {
log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name);
return r;
}
}
/* Verify description */
if (isempty(description) || streq(description, "-")) {
free(description);
description = NULL;
}
if (description) {
if (!valid_gecos(description)) {
log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, description);
return -EINVAL;
}
}
/* Verify home */
if (isempty(home) || streq(home, "-")) {
free(home);
home = NULL;
}
if (home) {
if (!valid_home(home)) {
log_error("[%s:%u] '%s' is not a valid home directory field.", fname, line, home);
return -EINVAL;
}
}
switch (action[0]) {
case ADD_RANGE:
if (resolved_name) {
log_error("[%s:%u] Lines of type 'r' don't take a name field.", fname, line);
return -EINVAL;
}
if (!resolved_id) {
log_error("[%s:%u] Lines of type 'r' require a ID range in the third field.", fname, line);
return -EINVAL;
}
if (description) {
log_error("[%s:%u] Lines of type 'r' don't take a GECOS field.", fname, line);
return -EINVAL;
}
if (home) {
log_error("[%s:%u] Lines of type 'r' don't take a home directory field.", fname, line);
return -EINVAL;
}
r = uid_range_add_str(&uid_range, &n_uid_range, resolved_id);
if (r < 0) {
log_error("[%s:%u] Invalid UID range %s.", fname, line, resolved_id);
return -EINVAL;
}
return 0;
case ADD_MEMBER: {
_cleanup_free_ char *resolved_id = NULL;
char **l;
/* Try to extend an existing member or group item */
if (!name) {
log_error("[%s:%u] Lines of type 'm' require a user name in the second field.", fname, line);
return -EINVAL;
}
if (!resolved_id) {
log_error("[%s:%u] Lines of type 'm' require a group name in the third field.", fname, line);
return -EINVAL;
}
if (!valid_user_group_name(resolved_id)) {
log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_id);
return -EINVAL;
}
if (description) {
log_error("[%s:%u] Lines of type 'm' don't take a GECOS field.", fname, line);
@ -1468,22 +1542,6 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
return -EINVAL;
}
if (!id) {
log_error("[%s:%u] Lines of type 'm' require a group name in the third field.", fname, line);
return -EINVAL;
}
r = specifier_printf(id, specifier_table, NULL, &resolved_id);
if (r < 0) {
log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name);
return r;
}
if (!valid_user_group_name(resolved_id)) {
log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_id);
return -EINVAL;
}
r = hashmap_ensure_allocated(&members, string_hash_func, string_compare_func);
if (r < 0)
return log_oom();
@ -1521,6 +1579,11 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
}
case ADD_USER:
if (!name) {
log_error("[%s:%u] Lines of type 'u' require a user name in the second field.", fname, line);
return -EINVAL;
}
r = hashmap_ensure_allocated(&users, string_hash_func, string_compare_func);
if (r < 0)
return log_oom();
@ -1529,14 +1592,14 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
if (!i)
return log_oom();
if (id) {
if (path_is_absolute(id)) {
i->uid_path = id;
id = NULL;
if (resolved_id) {
if (path_is_absolute(resolved_id)) {
i->uid_path = resolved_id;
resolved_id = NULL;
path_kill_slashes(i->uid_path);
} else {
r = parse_uid(id, &i->uid);
r = parse_uid(resolved_id, &i->uid);
if (r < 0) {
log_error("Failed to parse UID: %s", id);
return -EBADMSG;
@ -1546,30 +1609,20 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
}
}
if (description) {
if (!valid_gecos(description)) {
log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, description);
return -EINVAL;
}
i->description = description;
description = NULL;
i->description = description;
description = NULL;
}
if (home) {
if (!valid_home(home)) {
log_error("[%s:%u] '%s' is not a valid home directory field.", fname, line, home);
return -EINVAL;
}
i->home = home;
home = NULL;
}
i->home = home;
home = NULL;
h = users;
break;
case ADD_GROUP:
if (!name) {
log_error("[%s:%u] Lines of type 'g' require a user name in the second field.", fname, line);
return -EINVAL;
}
if (description) {
log_error("[%s:%u] Lines of type 'g' don't take a GECOS field.", fname, line);
@ -1589,14 +1642,14 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
if (!i)
return log_oom();
if (id) {
if (path_is_absolute(id)) {
i->gid_path = id;
id = NULL;
if (resolved_id) {
if (path_is_absolute(resolved_id)) {
i->gid_path = resolved_id;
resolved_id = NULL;
path_kill_slashes(i->gid_path);
} else {
r = parse_gid(id, &i->gid);
r = parse_gid(resolved_id, &i->gid);
if (r < 0) {
log_error("Failed to parse GID: %s", id);
return -EBADMSG;
@ -1608,6 +1661,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) {
h = groups;
break;
default:
return -EBADMSG;
}
@ -1812,6 +1866,15 @@ int main(int argc, char *argv[]) {
}
}
if (!uid_range) {
/* Default to default range of 1..SYSTEMD_UID_MAX */
r = uid_range_add(&uid_range, &n_uid_range, 1, SYSTEM_UID_MAX);
if (r < 0) {
log_oom();
goto finish;
}
}
r = add_implicit();
if (r < 0)
goto finish;

91
src/test/test-uid-range.c Normal file
View File

@ -0,0 +1,91 @@
/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
/***
This file is part of systemd.
Copyright 2014 Lennart Poettering
systemd is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
systemd is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/
#include <stddef.h>
#include "util.h"
#include "uid-range.h"
int main(int argc, char *argv[]) {
_cleanup_free_ UidRange *p = NULL;
unsigned n = 0;
uid_t search;
assert_se(uid_range_add_str(&p, &n, "500-999") >= 0);
assert_se(n == 1);
assert_se(p[0].start == 500);
assert_se(p[0].nr = 500);
assert_se(!uid_range_contains(p, n, 499));
assert_se(uid_range_contains(p, n, 500));
assert_se(uid_range_contains(p, n, 999));
assert_se(!uid_range_contains(p, n, 1000));
search = (uid_t) -1;
assert_se(uid_range_next_lower(p, n, &search));
assert_se(search == 999);
assert_se(uid_range_next_lower(p, n, &search));
assert_se(search == 998);
search = 501;
assert_se(uid_range_next_lower(p, n, &search));
assert_se(search == 500);
assert_se(uid_range_next_lower(p, n, &search) == -EBUSY);
assert_se(uid_range_add_str(&p, &n, "1000") >= 0);
assert_se(n == 1);
assert_se(p[0].start == 500);
assert_se(p[0].nr = 501);
assert_se(uid_range_add_str(&p, &n, "30-40") >= 0);
assert_se(n == 2);
assert_se(p[0].start == 30);
assert_se(p[0].nr = 11);
assert_se(p[1].start == 500);
assert_se(p[1].nr = 501);
assert_se(uid_range_add_str(&p, &n, "60-70") >= 0);
assert_se(n == 3);
assert_se(p[0].start == 30);
assert_se(p[0].nr = 11);
assert_se(p[1].start == 60);
assert_se(p[1].nr = 11);
assert_se(p[2].start == 500);
assert_se(p[2].nr = 501);
assert_se(uid_range_add_str(&p, &n, "20-2000") >= 0);
assert_se(n == 1);
assert_se(p[0].start == 20);
assert_se(p[0].nr = 1981);
assert_se(uid_range_add_str(&p, &n, "2002") >= 0);
assert_se(n == 2);
assert_se(p[0].start == 20);
assert_se(p[0].nr = 1981);
assert_se(p[1].start == 2002);
assert_se(p[1].nr = 1);
assert_se(uid_range_add_str(&p, &n, "2001") >= 0);
assert_se(n == 1);
assert_se(p[0].start == 20);
assert_se(p[0].nr = 1983);
return 0;
}